Conversation
Capability and error DTOs could be accepted with weaker meaning than the stage 1A contract claims: - AgentServiceCapabilitiesSchema required neither a complete nor a non-empty set, so an omitted Desktop-only capability was indistinguishable from a supported one. Every declared id is now required exactly once while unknown, duplicate, and over-limit sets stay rejected. - capability_unavailable did not require the capability, and any code could carry it. The code now owns the structured fields: capability_unavailable requires capability plus requiredClient (nullable, as in the capability DTO), and every other code rejects both, so a client never falls back to message. - The wire envelope inherited AppError's unbounded message and details. message is now non-empty and capped at 4096, and details is bounded in key count, key shape, key length, value depth, and value size, with credential-, path-, handle-, and identity-shaped keys refused. The mapping from service error codes onto LOCAL_CONTROL_ERROR_CODES is explicit in this file and total by construction; localControl.ts is unchanged.
- Measure the detail value budget on the value's own JSON UTF-8 encoding, so a nested key or array element can no longer hide bytes from the size limit, and run the depth walk plus the encoding check ahead of `JsonValueSchema` so an unmeasurable or pathologically deep value fails closed instead of escaping as a RangeError from the recursive JSON walk. - Drop the forbidden detail-key fragment and path blacklists. They rejected valid diagnostics such as `tokenCount` and `maxTokens` and promised a redaction no schema can perform; key syntax, key length, key count, and the depth/byte budgets stay, and the comment now states that sensitive-data redaction is the producer's responsibility. - State in the local-control mapping comment that `capability_unavailable` and `service_unavailable` both collapse onto `unavailable`, so the capability identity must be preserved by the adapter instead of read back from the code. - Extend the contract suite with the nested-key, large-array, boundary, fail-closed, and diagnostic-field cases.
An accessor- or proxy-bearing in-process value could be read more than once: the depth and size pre-checks measured one reading while the piped `JsonValueSchema` validated another, so a value whose later reads grew past the budget was accepted, and a getter or proxy trap that threw on a later read escaped `safeParse` as an uncaught exception. - Read each detail value once, through property descriptors, into a fresh plain JSON copy, and measure the size budget on that copy, so the bytes that were budgeted are the bytes a caller receives. - Reject whatever is not plain JSON data: accessors, array holes, non-Object.prototype prototypes, symbol keys, functions, bigints, undefined, and non-finite numbers, and turn any throwing read or measurement into a schema issue instead of an exception. - Extend the contract suite with the changing accessor, throwing accessor, throwing proxy, and non-plain object cases, plus the multi-byte UTF-8 size boundary.
Stage 0 record for the standalone agent service, at checked revision 7e758ab (plus the 4c07c5b architecture baseline and the four Stage 1A contract commits). baseline.md freezes the Desktop and direct ACP flows, the identifier and ownership matrices, the capability classification with the first-version allowlist, resource ownership through shutdown, the portable-import probe evidence, the invariants, and the Stage 1 handoff. Two blockers are recorded instead of claimed as passes: - The minimum two-turn/tool-continuation scenario was not completed: Node 22.22.0 is outside the declared >=24.18.0 <25, the installed Electron 41.10.4 does not match the lockfile 43.6.0, and there is no headless composition root to run it against. - safeStorage is unavailable outside a full Electron runtime and is imported at module scope by the credential store, so credential access blocks Stage 3. plan.md Stage 0 checkboxes are flipped per evidence, the unproven scenario is satisfied only through its blocker branch, and the reviewed-inventory line stays unchecked pending review. A Stage 1 sub-slice rule records 1A DTO-only, 1B events/interaction/cancellation, 1C client adapters, and 1D compatibility mapping so the delegation boundary lives in the repository. Documentation only: no production code, test, or configuration change.
Adds the Stage 1B client-facing DTOs and nothing else: the typed event envelope, bounded event subscription with epoch:seq cursor replay, overflow and resync semantics, the authoritative snapshot, owned artifact references, interaction/approval request-response DTOs, and three separate cancellation layers. Events reuse LocalControlEventCursorSchema and the Stage 1A ids. Every object is strict and every union discriminated, so unknown fields, wrong discriminators, missing cursors or decisions, illegal cancel layers, and oversized payloads fail closed; no principal, approver, renderer, handle, path, or runtime object is representable. Nothing imports these modules. Verification: test/main/contracts (71 passed, 14 new), targeted tsc over the new files, oxfmt --check, oxlint. Full typecheck:node/web fails only on pre-existing dependency drift (tokenx, @ai-sdk/open-responses) reproduced in the untouched main checkout; host Node is 22.22.0 against engines >=24.18.0.
Event data is read into a bounded plain copy before `JsonValueSchema` sees it. The recursive schema threw `RangeError` out of `safeParse` for a payload deep enough to exhaust the stack (a `JSON.parse` result at depth 4000 or 40000), which is not fail-closed. The read is iterative and bounded in depth, node count, and encoded bytes, refuses cycles, array holes, accessors, symbol keys, non-plain prototypes, and non-JSON values, and measures the copy it returns, so the byte budget is spent on what a client receives. Replay validation requires every replayed event to stay in the requested cursor epoch, not only the first: a switch in the middle with contiguous sequences and a matching `initialCursor` used to pass as one gap-free catch-up. A snapshot's pending interactions must belong to the snapshot's session, and `expired` is no longer accepted with `resumed: true`, because an expiry records a response that was not applied. Verification: test/main/contracts 74 passed (17 in the event contract; 3 new tests); the new cases fail against the previous sources (RangeError, mid-replay epoch switch, cross-session pending interaction, expired+resumed). oxfmt --check and oxlint clean. typecheck:node/web fail only on pre-existing dependency drift (tokenx, @ai-sdk/open-responses) plus pre-existing renderer errors; host Node is 22.22.0 against engines >=24.18.0.
The event-data read pushed a frame for every element or key of a container before it looked at the node budget, so the input's width, not the budget, decided how much memory the read materialized: a five-million-element array was enumerated in full (10,000,002 own-key and descriptor reads over 13.25s) before being refused as oversized, and the comment claiming the node check "stops an oversized payload from being copied in full first" was not true for width. A container is now refused before it is expanded when its width cannot fit the node budget next to the nodes already counted. The node budget is the byte budget — every node costs at least one encoded byte plus the delimiter joining it to its parent — so this is a work bound, not a second acceptance rule: a payload the byte check accepts has at most ~131k nodes, and the node check could never have refused one the byte check accepts. An array's width comes from `length`, so a wide array is refused without even listing its keys; an object's key list is what `Object.keys` returns, so only that list is still built at the input's own width. Frames held at once are now bounded by the depth budget times the node budget instead of by the input. An own `__proto__` key is refused at any depth instead of being copied. `Object.defineProperty` kept the key in the measured copy, but the record stage the copy is piped into writes keys by assignment, and `__proto__` is the one key where that sets a prototype instead of creating an own property, so the returned DTO was a different value from the one that was measured: `JSON.parse` of a document with that key was accepted and came back without it. `-0` is refused too: it is encodable but not preserved (`JSON.stringify(-0)` is `'0'`), so it does not survive the round trip the byte budget measures. Every other number semantic is unchanged, and -0 is the only finite double JSON does not reproduce. Verification: test/main/contracts 77 passed (20 in the event contract, 3 new cases). Against the previous sources the new cases fail: `__proto__` and `-0` are "accepted", the wide-array case reads all 10,000,002 keys and descriptors over 13.25s where the new suite runs in 173ms, and the nested budget case visits 131,071 children before refusing where it now visits none. oxfmt --check and oxlint clean. typecheck:node (9) and typecheck:web (5) report only the pre-existing dependency drift (tokenx, @ai-sdk/open-responses) and pre-existing renderer errors, with host Node 22.22.0 against engines >=24.18.0.
Adds the Stage 1C client-facing adapter boundary and nothing else: the typed handshake, submission, and submission-query DTOs, one closed operation vocabulary, the operation-to-capability mapping, one pure refusal resolver, and the adapter surface a binding implements. Stage 1A/1B are imported unchanged and no runtime, transport, Electron, ACP runtime, or CLI implementation is added. The submission DTO is the piece 1A/1B left open: 1A defined the receipt but no request, so a submission had no idempotency identity to be receipted against. `submissionId` is required, and a separate query request reads the receipt a lost response would have carried, because a missing receipt is not proof that no run started. The submitted text shares the snapshot's message bound, so a submission a client may make is one the transcript can report back. One adapter value with seven DTO-facing operations serves both bindings instead of one interface per binding. Their feature differences stay visible as data: the handshake returns the service's complete capability statement, an operation that depends on a capability is refused through `resolveClientOperationRefusal`, and `requiredClient` is copied from that advertisement rather than chosen by the caller. A capability no client can supply reports null; absent, unavailable, or self-contradicting advertisements refuse instead of reading as support, so a missing capability can never be mistaken for a working one. Nothing in the surface or in any request schema can carry an identity: no principal, renderer, approver, clientKind, asDesktop, callback, AbortSignal, handle, or absolute path is representable, and every object is strict so such a field is rejected. Verification: test/main/contracts 114 passed (37 new in agentServiceClientContract.test.ts); full test/main 9137 passed, 5 skipped, 1 file skipped. `pnpm run lint` and `pnpm run i18n` clean; `oxfmt --check .` clean over 3010 files; typecheck:node and typecheck:web both clean on Node 24.18.0. The repo's default typecheck gate does not include `test/**`, so the file's type-level assertions were checked with a temporary tsconfig extending tsconfig.node.json (test/main/contracts plus the agent-service contracts, removed afterwards): clean, and it fails when the surface is mutated to take an options bag with an AbortSignal. Ablation: the shared surface is one interface plus one resolver, and the resolver is the only place that decides a refusal, so an adapter cannot invent a required client at a call site. Rejected as unproven: a generic RPC envelope, an adapter registry or service locator, a capability index object cached per adapter, a per-binding interface pair, a handshake echo of the requested version under exact negotiation, a capabilities accessor beside the handshake, a caller-supplied refusal builder, an AsyncIterable subscription (live delivery is the transport's, Stage 4), and session lifecycle operations. The two fakes in the suite are fixtures, not shipped adapters, and a dead capability flag in one of them was made live or removed instead of left as decoration.
Four corrections and two boundary records on top of the Stage 1C client adapter commit. No Stage 1A/1B DTO change, and no runtime, transport, Electron, ACP, CLI, event, or database code. **A capability advertisement must agree with itself.** `common.ts` validates `reason` and `requiredClient` independently, so an advertisement could state `requires_desktop_client` without naming a client, or claim `requiredClient: 'desktop'` under `not_supported`. The handshake result now refuses a self-contradicting set instead of repairing it, and `resolveCapabilityRefusal` copies `requiredClient` only from a consistent entry: everything else refuses with `null`, so no caller is pointed at a Desktop lease the advertisement's own reason denies. **The contract test's type assertions are compiled.** `expectTypeOf` is erased at runtime and `tsconfig.node.json` covers `src/**` only, so the statements that this surface takes exactly these DTOs were enforced by nothing. `typecheck:contracts` compiles the client contract test together with the agent-service contract sources, is wired into `typecheck`, and fails when `submit` takes an options bag with an `AbortSignal` or a public request type gains a host type. **A binding that keeps no receipt no longer answers `not_found`.** A receipt query now answers an outcome: `receipt`, or `receipt_not_retained` when the binding cannot answer at all. `not_found` for a submission the binding accepted and executed reads as "no run started", which is the reading that licenses the resubmit this path exists to prevent. No new error code was added, and this is a value rather than a capability refusal because `session.persistence` gates the query while the same binding must keep advertising it to express a snapshot; a receipt-retention capability id would be a Stage 1A vocabulary change. **An interaction answer is addressed.** The 1B response DTO names its session, interaction, message, and tool call but no service instance, and one client can hold adapters for more than one instance. `respond` now takes a strict client-side envelope stating the instance plus the run and request the published interaction belongs to. No principal, approver, renderer, or client-kind field exists in it, and the 1B file is unchanged. **Hand-off, where the next slice can cite it.** `client.ts` records that live delivery is the local transport slice's concern and that no `AsyncIterable`, callback, or long-connection handle is introduced here, and that steering, the pending-input queue, and session lifecycle are not client operations on this surface. A contract test pins both boundaries to the frozen seven-operation vocabulary. `plan.md` is deliberately untouched. Verification: `test/main/contracts` 121 passed (44 in the client contract test); full `test/main` 9144 passed, 5 skipped, 1 file skipped; `typecheck:contracts`, `typecheck:node`, `typecheck:web`, and the composed `typecheck` clean; `oxfmt --check .` clean over 3010 files; `lint` and `i18n` clean. Ablation: reverting the consistency guard fails the two capability tests; reverting the receipt-retention outcome fails the two query tests; dropping the instance check in `respond` fails the addressing test; and the new gate exits non-zero for a `submit` that takes an `AbortSignal` options bag and for a submission request that gains `z.instanceof(AbortSignal)`.
The gate's diagnostic filter keeps only diagnostics that carry a scoped
file, so TS6053 ("File ... not found.") for an absent root was dropped,
the program simply got smaller, and the gate exited 0 — losing the
`expectTypeOf` assertions it exists to enforce. Scoped roots are now
checked against the filesystem before compiling, and the failure names
the missing relative path on stderr.
The root check used `ts.sys.fileExists`, which is true for a `chmod 000` root, so an unreadable root still compiled a smaller program and the gate exited 0 — the false green the missing-file check was meant to close. Roots are now read: an undefined `ts.sys.readFile` marks the root absent or unreadable, the report keeps the two apart, and stderr names the relative path. The gate regression test covers the unreadable root with a real `chmod 000` (restored in `finally`, mode and content asserted), skips with a note where mode bits are not enforced, drops the fragile empty-stderr assertion, and normalizes separators in path expectations.
The gate compiled a smaller program and reported a pass whenever a scoped root could not be read: the scope filter dropped file-less diagnostics, which is where TypeScript puts the config, option, global, and missing-root TS6053 errors, so only in-scope file diagnostics survived. File-less diagnostics are now kept alongside `parsed.errors`, and the root readability check stays for the missing/unreadable distinction. A config using a deprecated compiler option now fails on TS5101 instead of passing. The regression test no longer deletes or chmods the tracked contract test. Each case runs a copy of the real script in a `mkdtemp` fixture holding a minimal tsconfig, a scoped source, a scoped contract test, and a junction to the resolved `node_modules`: pass, missing root, unreadable root, in-scope type error, unparseable config. Path expectations normalize `\` so they match `path.relative` output on Windows, and success asserts the pass line rather than an empty stderr. Verification (Node 24.18.0): `typecheck:contracts` and composed `typecheck` exit 0; `test/main/contracts` + `test/main/scripts` 38 files / 382 tests pass, with the tracked client contract test byte-, mode-, and inode-identical before and after the run and no fixture left in the temp dir; `lint`, `i18n`, and `oxfmt --check .` (3010 files) clean. Ablation, in fixtures: a `fileExists`-only root check exits 0 for an unreadable root, and removing the root check while re-filtering file-less diagnostics exits 0 for a missing root — both fail the suite with "expected 0 to be 1". Dropping in-scope file diagnostics fails the type error case.
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Integrate upstream dev at 8cdf580 (merge base 5194235). Eight content conflicts resolved keeping host re-exports while porting upstream stream-revision changes into the kernel runtime owners. Includes the M0 baseline corrections: kernel path mapping in tsconfig.node.json resolves inside the repository, the Vitest 4 contract gate declares its node types explicitly, and architecture baselines are regenerated from the merged tree.
Remove the 175 pure host re-export shims and rewrite production callers to supported @deepchat/agent-kernel subpath exports. Add the kernel-port structure typecheck gate that enforces the port pairing contracts at compile time.
Promote 127 exact public subpaths into @deepchat/shared with manifest-derived staging, isolated consumer closure, and exported declaration validation. Fix the gate defects found in review: unexported subpaths, relative and symlink artifact escapes, dependency-cycle overflow, and Windows path separators. Add the manifest-completeness regression guard.
The host chat barrel re-exported MessageFile through its own @shared/chat barrel, resolving to any under skipLibCheck. Target the package owner and add a compiler-symbol identity regression that rejects any/unknown substitutions.
Relocate the Desktop app into packages/desktop following the M0 path contract: config-relative aliases, app-local out/ outputs with Builder staging under Desktop, explicit Desktop cwd for Builder, root-owned lockfile and pnpm policy. Root tooling stays repository-wide.
Correct the 52-path relocation delta: CI helper imports and workflow paths, native dependency resolution from the owning importer, stale path and command assertions, and the xlsx integrity restoration.
Complete packages/cli with exported launcher artifacts and global help/version output. Relocate the shared and kernel test suites into their owning packages, reconcile formatting over the 301 formerly excluded UI files, and wire the root aggregate Vitest configuration across all projects.
Inject a complete synchronous transaction capability at the session composition boundary. Tape fact, transcript projection, cursor, usage and pending-input state with linked-message atomicity stay on one connection; no generic persistence package is added.
Extract the portable provider runtime with injected config, credential and OAuth ports (M5a) and the MCP transport, session and process core on explicit ports with bounded stdio, token binding and interactive refusal (M5b). Relocate the shared DeepSeek, failure and trace values to @deepchat/shared and declare @types/json-schema exactly.
Restore the mandatory root MCP artifact preflight with real stdio and HTTP transports from staged artifact bytes. Add manifest-derived cold source typechecks for provider and mcp, cross platform compiler invocation through process.execPath, and the staged declaration and runtime consumer checks.
The root aggregate resolved the desktop script tests' relative imports as /scripts paths because the inline projects had no explicit root; pin both projects to the app root. The kernel and provider artifact closures rebuild the same shared dist, so move the two writer tests into a dedicated serial artifact project and include it in test:main. Use fileURLToPath for the include patterns so spaces and Windows drive paths cannot silently drop the artifact tests.
Record the executed milestone evidence in the migration plan and spec, and align AGENTS and README guidance with the actual engine requirements (Node >=24.18.0 <25, pnpm >=10.34.5 <11) enforced by the root package.json.
Integrate dev ebfe83a (17 commits): the sync host endpoint feature, memory atomicity fixes, and the Baizhi MCP example plugin. Relocation porting: the sync host module lands at packages/desktop/src/main/sync/host with its tests under packages/desktop/test/main/sync/host; the new syncHost contracts stay desktop-owned beside the host routes barrel; rename detection carried the memory, composition, logging and plugin test deltas; the Baizhi example test resolves the workspace root relative to the relocated test file. The merge touches neither the renderer surface nor any extracted package manifest. Post-merge gates: full typecheck including the kernel-port structure gate, the affected sync, memory and plugin suites, format, i18n, lint, and the full main aggregate (661 files / 9309 tests) all pass.
The compatibility projection created its own AbortController, so echo, dispatch and the runtime helpers observed a signal that never aborted when the ACP turn was cancelled, timed out or the peer exited. The projection port now requires the caller's signal: AcpAgentInstance passes the active prompt controller's signal into begin(), and the adapter forwards it into IoParams instead of creating a substitute. Regression: cancelling the active prompt aborts the captured projection signal and reaches projection.cancel.
closeAll could hang forever when an ACP peer never answered cancel: instance.close awaits cancel which awaits the peer, and the runtime had no upper bound before its own allSettled. Every closeAll phase now settles within a bounded timeout, leaving uncooperative close work detached while the runtime maps are cleared; process-level termination stays owned by the runtime owner's process manager shutdown. Regression: a hanging instance close no longer stalls closeAll.
plugin.mjs and package-plugin.mjs resolved build/bundled-plugins, dist/plugins and the native build script through process.cwd(), so a root or release-helper invocation would read or write the wrong tree and break root/filtered command equivalence. All three paths now resolve from the script's own app root. plugin:bundle:clean becomes a 'clean' action in plugin.mjs with the same config-relative targets instead of an inline cwd-dependent node -e command.
The root build produced shared, provider and kernel artifacts but never built @deepchat/mcp, so a fresh checkout or a consumer running only the standard root build could package against a missing or stale MCP dist. build:mcp now runs in the root chain between provider and kernel.
generateCompletionStandalone swallowed auth, network and provider configuration errors by default and returned an empty string, which a caller cannot distinguish from an empty completion. The portable core now fails fast; swallowing is an explicit opt-in whose diagnostics log only provider and model ids. generateCompletion logged the full message array; it now records provider, model, temperature, maxTokens and message count only. All existing callers already opt out of swallowing; the desktop vision analysis path reaches its existing metadata degradation handler with the error message instead of an empty result.
The root export depends on node: builtins, process.env and stdio process lifecycle but advertised nothing; the manifest description and the index doc comment now declare the surface Node-only. Default diagnostics on connect, registry, transport, sampling, elicitation, tool, prompt and resource paths logged raw error objects and remote text that can carry URLs, auth hints, commands or user input; they now record the operation, server id and an error category only. Errors still propagate to callers unchanged.
The CLI guide's ownership table still pointed at the pre-migration root layout; it now names packages/cli, packages/desktop/src/main/cli, the shared wire contracts versus the Desktop adapter contracts, and the kernel-owned command permission service. The standalone tracker's kernel build record notes that the emitted-file count shrank from 492 at 2B-3a time to the current 348.
Normal generated registry refreshes produced by the root build chain.
serverLastErrors fed the renderer's lastError display and the failed status event crossed IPC, both carrying raw error messages that can include remote text, URLs, paths or auth hints. Both now carry the fixed error category; the schema-validation warning and the model display-name warning use the same category helper so the boundary is uniform across console, diagnostics state and status events. Errors still propagate unchanged to callers and protocol responses.
The generated providers.json dropped the upstream updated_at, so a refresh could not be audited against its source revision. The sanitizer now preserves it as source_updated_at. The carried refresh was audited against PublicProviderConf dev e67c8272: all ten removed model IDs are absent upstream and the remaining drift is upstream movement after the snapshot.
Record the two post-merge review rounds and the registry refresh audit trail.
The CLI could only be debugged through one-shot rebuilds; its runtime specifiers resolve workspace packages, so running TypeScript sources directly is not possible. cli:dev builds out/cli/deepchat.mjs in watch mode with the desktop version, and the CLI package gains its own dev script writing dist/ with a dev version. The guide documents the loop: run the watcher and execute the artifact against a running Desktop.
Replace the watch-mode dev build with the straightforward loop the workflow needs: cli:build bundles out/cli/deepchat.mjs, cli:run executes it with plain Node and forwards arguments. run-cli.mjs strips the leading -- that nested pnpm run forwarding preserves verbatim, fails fast with a clear message when the bundle is missing, and propagates the CLI exit code. The guide documents the two-step loop.
The packaging workflows pass repository-root-relative paths (extension-path, extension-base64-path, resources-path, report-path, plugin-root) to desktop scripts that pnpm --filter executes from the desktop package, so path.resolve doubled the packages/desktop segment and the Windows VSS verification failed on the first real six-target run of the relocated tree. The scripts now derive the repository root from their own location and anchor those arguments there, making resolution cwd-independent; absolute inputs and the script-owned defaults are unchanged. Verified by running the VSS smoke from both the repository root and the desktop package with the same repository-relative argument.
Integrate dev a79f884 (5 commits): the AnonRouter provider, plugin settings window route scoping, and the memory drain and scoped retrieval fixes. Relocation porting: the AnonRouter registry entry lands in packages/provider and its renderer icon and new tests land under packages/desktop; rename detection carried the memory, plugin, provider and preload deltas. Conflict resolution keeps the kernel and shared package import paths while adopting dev's new imports (MAINTENANCE_DRAIN_TIMEOUT_MS, WORKING_REFRESH_DEBOUNCE_MS, withSoftDeadline via the kernel memory core). Post-merge gates: affected suites (285 tests), full typecheck, format, lint, i18n and the full main aggregate all pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch delivers the repository and ownership groundwork for a portable DeepChat agent service. It is intentionally a structural extraction and contract-freeze PR for stages 0–2, not the standalone service product itself.
The current and future execution paths are deliberately separate:
The branch preserves the current Desktop compatibility path. It does not add a standalone service process, a new transport or authentication implementation, a Desktop/CLI cutover, or a new UI. Those belong to later standalone-service stages.
Scope at a glance
origin/dev+76,070 / -25,187architecture/standalone-agent-harnessdevdab4b3b15>=24.18.0 <25, pnpm>=10.34.5 <11The large file count is primarily a deliberate monorepo relocation and ownership change: Desktop sources/tests/resources move under
packages/desktop, while shared contracts, CLI, provider, MCP, and kernel code become explicit workspace package surfaces. This is not a product-feature diff of the same size.What changed
1. Standalone architecture and contract freeze — Stages 0–1
Added the normative standalone-agent architecture, baseline, compatibility mapping, and execution tracker under:
docs/architecture/standalone-agent-harness/spec.mddocs/architecture/standalone-agent-harness/plan.mddocs/architecture/standalone-agent-harness/baseline.mddocs/architecture/standalone-agent-harness/compatibility.mdThe client-facing contract is deliberately serializable and host-neutral. It covers the initial vocabulary for:
capability_unavailablebehavior and compatibility mapping to the maintained local-control CLI.The public DTOs do not carry Electron objects, callbacks,
AbortSignal, provider clients, database connections, principals, renderer handles, absolute paths, or runtime class instances.typecheck:contractscompiles the contract assertions so these boundaries are enforced rather than documented only.The Stage 1 Agent Service DTO/client contracts currently remain under
packages/desktop/src/shared/contracts/agent-service. Promoted local-control wire contracts live inpackages/shared. Host-neutral contract design does not imply service wiring or publication through@deepchat/shared.The current client contract is intentionally incomplete: live delivery, session lifecycle, steering, interruption, pending-input queue operations, and compatibility mapping are explicit hand-offs to later slices rather than claims that a final service API already exists. A missing or pruned receipt is also not evidence that execution never started; a binding that cannot retain receipts reports
receipt_not_retained, andnot_founddoes not authorize blind resubmission.2. ACP ownership and lifecycle seams
The direct ACP backend is separated from the built-in Desktop agent ownership model:
SessionStatePort/AcpSessionStateAdapterinstead of hydrating built-in scope.AbortControlleris created.closeAll()has bounded shutdown phases. An uncooperative peer cannot block shutdown forever; pending work is detached after the phase timeout and runtime maps are cleared.The direct ACP backend continues to use its own
AcpAgentRuntime. External ACP does not execute through the built-in@deepchat/agent-kernelloop. Existing Desktop and direct ACP flows retain their respective host seams; these changes close ownership and lifecycle leaks without merging the two runtimes.3. Kernel extraction into
@deepchat/agent-kernelThe portable built-in agent kernel is now a private workspace package:
packages/agent-kernel@deepchat/agent-kernelThe old host re-export layer was removed rather than preserved as an invisible public API. Production callers now use supported package exports. The kernel depends on explicit structural ports instead of
Pick<HostClass>or Desktop implementation types.The package is embedded by Desktop through the workspace link, so the current Desktop path still runs the same kernel implementation. A future service host can consume the package without importing Electron, but that host composition is not implemented by this PR.
4. Shared package extraction —
@deepchat/sharedThe reviewed neutral closure is now owned by
packages/shared:@deepchat/shared;any/unknownsubstitutions, including theMessageFilebarrel case.The package boundary is based on actual consumers, not a speculative extraction of all historical
src/sharedfiles.5. Desktop relocation into a normal workspace package
Desktop is now a workspace member at
packages/desktopwhile retaining its product identity (name: DeepChat):packages/desktop/out/{main,preload,renderer,cli}) and Builder staging remains under Desktop;No UI behavior change is intended. Desktop still owns real application composition, ACP lifecycle, built-in host adapters, local-control server behavior, and the Desktop-owned Agent Service contract gate. The relocation is therefore not only a path/config move; those runtime boundaries remain part of the behavior review.
6. CLI package and launcher boundaries
Added
packages/clias the explicit CLI client/launcher package boundary:--help/--versionbehavior;The current CLI bundle itself can be executed with system Node during development after it has been built. The installed-launcher runtime-discovery rule must not be confused with a general Electron-only restriction on the bundle:
The current local development loop is deliberately explicit:
pnpm run cli:buildbundlespackages/desktop/out/cli/deepchat.mjs, using the Desktop version;pnpm run cli:run -- --helpandpnpm run cli:run -- profile listexecute that bundle with plain Node and forward its arguments;packages/desktop/scripts/run-cli.mjsfails fast when the bundle is missing, strips the nested--forwarding marker, and propagates the child exit code;The preceding watch-mode implementation was replaced by this build-then-run loop in the latest commit. This branch does not turn the CLI into the standalone service. Stage 3 service-host composition, profile ownership, headless credential/OAuth ownership, and Desktop/CLI cutover remain future work.
7. Provider and MCP portable cores
Extracted provider and MCP runtime code behind explicit ports:
MCP diagnostics are intentionally split by boundary:
8. Build, test, CI, and artifact gates
Added or hardened the package and repository gates:
distraces;These gates are intended to prove both sides of each package boundary:
typecheck:contractsis static boundary evidence. It does not by itself prove that a service transport, authentication layer, or standalone host has been implemented.9. Provider/ACP registry refresh and provenance
The branch retains the generated provider and ACP registry refresh required by the normal build flow.
The provider refresh was audited against:
ThinkInAIXYZ/PublicProviderConfdeve67c827216525474...The ten model removals from the carried refresh were independently verified as absent from upstream at that commit. The sanitizer now preserves a future upstream
updated_atassource_updated_atso subsequent generated snapshots can carry source provenance.The current checked-in
providers.jsonpredates that metadata change and does not yet containsource_updated_at. This is a non-blocking P2 follow-up for the next registry refresh, not a runtime compatibility issue.Runtime behavior: before and after
Current Desktop built-in path
Direct ACP path
The direct ACP runtime remains a separate peer path; it is not routed through the built-in kernel loop.
ACP cancellation
Cancellation, timeout, and peer-exit now reach the projection that owns the work.
Provider failure
MCP diagnostics
Shutdown
What this PR does not do
These are intentionally outside this branch:
safeStoragecompatibility strategy;The structural implementations and contract slices are present, with local and independently reviewed evidence recorded below. This does not close all monorepo acceptance gates: applicable cross-platform packaging/native and launcher checks remain pending or environment-dependent. Standalone Stages 3–7 are separately unimplemented. In particular, the recorded final package evidence is unsigned macOS arm64; non-macOS targets, DMG/ZIP publication, signing, and update flows are not proven by that evidence.
Verification
Verification evidence is revision-scoped. The latest two commits are included in the scope statistics above:
ced528c5b(feat(cli): add a watch-mode dev build), subsequently superseded bydab4b3b15(feat(cli): add a local run command).The targeted checks below were run on the previously reviewed revision and were not implicitly re-run for every path after these CLI workflow commits.
Previously reviewed revision evidence
On the reviewed pre-watch revision, using Node
v24.18.0/ pnpmv10.34.5:pnpm run typecheckpnpm run build:mcp;pnpm run test:mcp:artifactpnpm run format:check;pnpm run i18n:validate;pnpm run lint— 0 errors, existing warnings only;git diff --check— clean for that reviewed revision.Recorded whole-branch / package evidence
The architecture plans also record broader evidence from the migration work, including main-process and renderer suites, the root build, and a specified unsigned macOS arm64 packaged smoke. Those records include 658 main files / 9,252 tests and 278 renderer files / 2,516 tests. They are historical, path-scoped evidence and should not be read as a claim that all of those checks were re-run at
ced528c5b.The two latest CLI workflow commits have not been claimed as independently revalidated by the targeted checks above. The documented development behavior on the current HEAD is:
The per-stage acceptance evidence, exact command history, known limitations, and future blockers remain recorded in:
docs/architecture/standalone-agent-harness/plan.mddocs/architecture/monorepo-migration/plan.mddocs/architecture/standalone-agent-harness/baseline.mddocs/architecture/standalone-agent-harness/compatibility.mdReviewer reading order
docs/architecture/standalone-agent-harness/spec.md(especially the two current execution paths and the prohibition on merging direct ACP into the built-in loop).docs/architecture/monorepo-migration/spec.mdfor package ownership, dependency, and compatibility constraints.packages/cli/package.json,packages/desktop/src/shared/contracts/agent-service, and the promotedpackages/sharedlocal-control contracts.packages/shared/package.json,packages/agent-kernel/package.json,packages/provider/package.json,packages/mcp/package.json, andpackages/cli/package.jsonfor package boundaries.packages/desktopfor path/config/output ownership and the retained composition, ACP lifecycle, host adapters, local-control server, and contract behavior.Review status
This PR has completed the intended structural extraction and contract slices for stages 0–2, with the boundaries and known observations recorded above. It should not be read as claiming that the standalone service host, all cross-platform packaging/native gates, or the final Desktop/CLI cutover already exist. The remaining standalone host work and any unclosed monorepo exit gates are explicitly tracked as separate follow-up work.