qwen-code/packages/cli
tanzhenxin 8d765fec78
refactor(core): TaskBase envelope + foreground subagent persistence (#3970)
* refactor(core): TaskBase envelope + foreground subagent persistence

Establishes a shared `TaskBase` envelope across the agent / shell /
monitor task registries with a mandatory `outputFile` field. Brings the
foreground subagent path into compliance with the new contract, so it
now leaves the same JSONL transcript + meta sidecar on disk that
backgrounded subagents have always produced — closing the only gap
where a registered task wrote nothing. Renames the agent-task
discriminator from `flavor: 'foreground' | 'background'` to claw-code's
`isBackgrounded: boolean`; the deprecated names are kept as
one-release type aliases.

PR 1 of the task-registry-unification design. PR 2 will collapse the
three per-kind registries into one thin TaskRegistry plus per-kind
modules.

* refactor(core): drop unused BackgroundTaskFlavor type alias

The alias only preserved the type name; no in-tree caller used it,
and after the field rename no realistic external consumer use survives
(reading entry.flavor / writing { flavor: ... } both fail at the use
site regardless of whether the alias resolves). Drop it instead of
carrying a hollow shim.

* fix(core): tighten foreground subagent launch path

- Register before writing the meta sidecar so a register() failure can't
  leave an orphaned 'running' meta file behind. writeAgentMeta is
  best-effort and never throws, so the inverse failure mode (registry
  entry without sidecar) is a benign degradation.
- Cache getGitBranch by cwd at the agent module level so foreground
  launches don't pay a fresh git rev-parse exec each time. Branches
  don't change within a process under normal use; the transcript
  annotation is best-effort audit metadata.
- Document on cancel() that foreground entries take a partial path
  through the method — Map deletion is the caller's responsibility
  via unregisterForeground() in the tool-call's finally path.

* fix(agent): correct foreground meta status mapping and register order

The foreground finally block in agent.ts mapped any non-ERROR, non-CANCELLED
terminate mode (including MAX_TURNS, TIMEOUT, SHUTDOWN) to 'completed' in
the sidecar, so post-mortem readers and resume logic saw a successful
status for runs that actually hit a guardrail. Flip the ternary to mirror
the background path: GOAL -> completed, CANCELLED -> cancelled, else ->
failed.

Also reorder the background launch so registry.register() runs before
writeAgentMeta(), matching the foreground path. Both paths now share the
same orphaned-meta guarantee.

* test(agent): rename stale foreground-flavor test

The "default flavor (absent) behaves as background" test name and its
backwards-compat comment referenced the old optional flavor field, but
the registration shape has required isBackgrounded for a while now —
there is no "absent" path to exercise. Rename it to describe what the
assertion actually covers: that background entries fire a task-
notification on complete.

* refactor(core): alias BackgroundTaskStatus to TaskStatus

The local `BackgroundTaskStatus` union was byte-identical to the new
shared `TaskStatus` defined in `tasks/types.ts`. Replace it with a
`@deprecated` type alias so external consumers (notably
`nonInteractiveCli.ts`) keep compiling unchanged while the canonical
name lives in one place.

* refactor(core): tidy monitorRegistry signatures and document cancel ordering

Two small consistency wins flagged in review:

1. `dispatchOwnerLifecycleWake` and `dispatchNotification` were the only
   methods on the registry still typed with the deprecated `MonitorEntry`
   alias. Rename their parameters to `MonitorTask` to match every other
   signature in the file.

2. `cancel()` orders `settle()` and `abort()` differently between its two
   branches, which is intentional (silent cancel locks the terminal status
   before abort listeners run; default cancel lets a naturally-completing
   operation settle through its own terminal path). Document that
   asymmetry in a JSDoc on the method so the next reader doesn't have to
   reverse-engineer it.

* refactor(core): migrate internal BackgroundTaskStatus refs to TaskStatus

The `BackgroundTaskStatus` alias was introduced in 91b59a8fb as a
`@deprecated` synonym for external SDK consumers (notably
`nonInteractiveCli.ts`). New internal references in this PR's own
file kept the old name; migrate them so the only remaining usage of
the deprecated alias is the alias declaration itself.

No behavior change — the alias is `= TaskStatus` so the union is
identical.

* test(agent): cover foreground failed-mode terminal status mapping

The foreground finally block maps GOAL→completed, CANCELLED→cancelled,
and everything else (ERROR, MAX_TURNS, TIMEOUT, SHUTDOWN) → failed.
Only the GOAL branch was asserted; the failed-mode fallback had no
coverage even though the same mapping recently regressed (d67da6d50)
and had to be fixed by review.

Adds a table-driven case mocking getTerminateMode to ERROR /
MAX_TURNS / TIMEOUT and asserting patchAgentMeta receives
status: 'failed'. CANCELLED is already covered by the
"foreground CANCELLED prefixes the partial result" test below.

* test(agent): cover foreground CANCELLED → cancelled meta mapping

Extends the foreground terminate-mode it.each to assert that
CANCELLED is recorded as `cancelled` in the on-disk sidecar — the
existing cancel-prefix test only verified the LLM-visible payload,
leaving the patchAgentMeta mapping uncovered. A regression flipping
CANCELLED → 'failed' would now fail this case.

* test(agent): make registry path assertions platform-agnostic

The outputFile/metaPath regexes hardcoded forward slashes, so the
foreground JSONL+meta reservation test failed on Windows where paths
use backslashes. Accept either separator.

* fix(core): guard executeBackground register-throw window; correct outputFile contract

A throwing register() subscriber in executeBackground() would leak the
already-spawned child + open output stream, unreachable by /tasks /
task_stop. Mirror the promote path's defensive try/catch: abort the
entry's controller, destroy the stream, and rethrow so the launch fails
visibly.

Also correct the TaskBase.outputFile contract: agent JSONL is
materialized on the writer's first append, which is the launch prompt
at attach time — not the first runtime event. A subagent cancelled
before any event still leaves a prompt-only JSONL plus meta, not meta
alone.
2026-05-16 22:53:08 +08:00
..
src refactor(core): TaskBase envelope + foreground subagent persistence (#3970) 2026-05-16 22:53:08 +08:00
index.ts fix(cli): stop double-wrapping and double-printing API errors in non-interactive mode (#3749) 2026-05-03 08:39:31 +08:00
package.json feat(core,cli): add generic atomicWriteFile, wire into Write/Edit tools, upgrade @types/node (#4096) 2026-05-15 17:52:50 +08:00
test-setup.ts perf(cli): code-split lowlight to cut startup V8 parse cost (#4070) 2026-05-15 17:26:18 +08:00
tsconfig.json fix(hooks): inject SessionStart additionalContext into chat context (#4115) 2026-05-15 15:21:25 +08:00
vitest.config.ts refactor(core): Unify package exports and improve dev experience 2026-02-01 11:59:05 +08:00