Commit graph

1463 commits

Author SHA1 Message Date
samuelhsin
f1a03e4928
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 23:10:58 +08:00
易良
b5577b7d11
fix(sdk): route unrecognized diagnostics onto a bounded transcript sidechannel (#9202)
* fix(sdk): route unrecognized diagnostics onto a bounded transcript sidechannel

Normalizer-classified unrecognized_event / unrecognized_session_update debug events no longer enter transcript blocks[]: they are mirrored onto a capped unrecognizedDiagnostics sidechannel instead. This stops them from finalizing a streaming assistant/thought block (which dropped a following assistant.usage frame) and from consuming the maxBlocks budget (which let repeated noise evict real conversation content). malformed_payload diagnostics and client-dispatched debug events keep their existing block semantics.

* fix(sdk): align browser bundle budget

* fix(sdk): close the sidechannel review round (#8823)

- export the sidechannel API through the daemon barrel
  (selectUnrecognizedDiagnostics, UNRECOGNIZED_DIAGNOSTICS_LIMIT,
  DAEMON_UI_UNRECOGNIZED_DIAGNOSTIC_REASONS + types) and pin the
  reachability in daemon-public-surface.test.ts
- restore the MAX_TEXT_BLOCK_LENGTH cap on sidechannel text, mirroring
  truncateText exactly (suffix fits within the cap)
- ship the unrecognized reason subset as a runtime const array and route
  by membership, so a new reason cannot fall through to appendStatusBlock
- copy the correlation fields createBase stamps (promptId, sourceRecordIds,
  branchRecordId, originatorClientId) onto sidechannel entries; drop the
  dead source/data switches
- un-fuse the budget-history comment chain in scripts/build.js
- update docs/developers/daemon-ui for the split routing
- tests: full entry shape, text cap, block-path debugReason counterpart,
  and a webui malformed_payload interleave sibling so the #7012
  flush-before-guard keeps a discriminating stimulus

* fix(sdk): address round-2 sidechannel review for #8823

- build.js: bump daemon browser bundle budget 191KB -> 192KB
  (195,591 bytes measured > 195,584 cap; build failed at head)
- webui: narrow the observer-mode debug guard so unrecognized_*
  diagnostics reach the reducer sidechannel; only block-path debug
  events are dropped
- webui: merge history-store unrecognizedDiagnostics in
  applyTranscriptHistory so paged-back sessions keep diagnostics
- transcript: extract truncateTextAtLimit shared by the block and
  sidechannel truncation paths
- transcript: reset unrecognizedDiagnostics on rewind alongside the
  sibling per-turn state resets
- types: rename DaemonUnrecognizedDiagnostic.receivedAt to
  clientReceivedAt (matches the sibling block projection)
- tests: reason-prefix conformance pin, rewind reset, narrowed guard,
  history pagination merge

* fix(webui): avoid flushing sidechannel diagnostics

* fix(sdk): preserve diagnostics across rewind

* fix(webui): dedupe sidechannel history records

* fix(webui): align the paging sidechannel test with the normalizer keys

The paging test added in e6b40e5c failed deterministically (webui
suite red, CI Test job red) for two reasons:

1. The fixtures stamped only _meta['qwen.session.recordId'], but the
   SDK normalizer's extractSourceRecordIds reads
   _meta.qwenTranscript.sourceRecordIds — no sidechannel entry ever
   carried sourceRecordIds, so the dedupe assertion could not pass and
   the new displayedRecordIds loop was never exercised by a passing
   test. Stamp BOTH keys, matching production replay frames
   (acp-bridge buildUpdateMeta) and the sibling dedupe test.
2. Cap arithmetic: LIMIT-1 live entries + 2 fresh history entries =
   LIMIT+1, so the newest-wins slice evicted record-old-1 which the
   test asserted present. Emit LIMIT-2 live events so the post-merge
   total lands exactly on the cap.

Also correct the post-merge index assertions: history entries come
first (old-1, old-2), then the deduped-once live overlap, then the
first live mystery event. Suite 506/506, eslint + prettier clean.

* fix(sdk): raise diagnostic sidechannel bundle budget

* fix(sdk): raise the daemon browser bundle budget to 198KB and pin the diagnostics selector

- The sidechannel routing + selector cost ~1037 B over the 197KB cap
  (bundle measured 201893 B), failing the browser-bundle size gate; bump
  MAX_DAEMON_BROWSER_BUNDLE_BYTES to 198 * 1024.
- Fold the rebase-residue 190→191→192 KB ledger entries into the accurate
  190→195→196→197→198 lineage so the next bump has one canonical history.
- Add a behavioral pin for selectUnrecognizedDiagnostics: it must return
  the routed sidechannel itself (toBe), discriminating a `return []` or
  shallow-copy regression that the typeof-only surface test cannot see;
  flip-verified.

* fix(sdk): reset the user pointer on sidechanneled diagnostics, share the routing predicate

appendUnrecognizedDiagnostic left activeUserBlockId untouched while the
replaced appendStatusBlock path reset it for every non-user block; a
later mergeable user.text.delta with no promptId stamp (e.g. a peer
client's $ <cmd> echo) then appended onto the earlier user block
across the diagnostic, collapsing two user turns into one and skewing
rewindTranscriptToUserTurn's kind==='user' turn indexing. Keep the
reset (assistant/thought pointers stay untouched, the point of the
sidechannel); witness test flip-verified red without the one-line reset.

Also export isUnrecognizedDiagnosticReason from types.ts next to
DAEMON_UI_UNRECOGNIZED_DIAGNOSTIC_REASONS and call it at all three
routing-guard sites (reducer, provider flush condition, provider drop
filter) so the #7012/#8823 guard pair classifies every debug event
against one source instead of three hand-written copies.

* fix(ci): prevent bite harness SIGPIPE

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-19 14:38:00 +00:00
Heyang Wang
5003ab3c7f
feat(web-shell): add transcript contract prevalidation (#9388)
* test(web-shell): add transcript contract prevalidation

Freeze reproducible evidence for current transcript paths before any
VS Code or HTML export production migration.

- Add versioned fixtures, closed export schema, and capability gates
- Probe direct-daemon and ACP identity under partial history prepend
- Preserve raw adapter semantics and full write_file Turn Output diffs
- Document the two-MR architecture, security constraints, and blockers

* fix(web-shell): harden transcript prevalidation gates

Make the evidence-only contract suite enforce the review assumptions it
documents while preserving the existing runtime transcript behavior.

- Run the contract suite in the required no-AK integration job
- Fail closed on ambiguous identity probes and deduplicate gate kinds
- Enforce manifest, hash, export safety, and renderer version boundaries
- Cover visible transcript text and stable Desktop packaging semantics
- Record the complete PR comment evaluation and verification outcome

* fix(web-shell): close transcript prevalidation gaps

* fix(web-shell): remove brittle Desktop wiring probe

Keep transcript contract prevalidation at the evidence level it can
actually prove. The previous source-text assertion could both reject
equivalent formatting and pass unreachable packaging code.

- Remove the Desktop script parser and its false behavioral claim
- Mark installed-artifact verification as deferred to Desktop smoke tests
- Clarify MR1 matrix, CI wiring, and provenance evidence boundaries
- Refresh the hash-locked capability matrix fixture

Note: This does not change Web Shell or Desktop production behavior.

---------

Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
2026-08-19 14:13:12 +00:00
samuelhsin
33c2f8ee98
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 20:23:51 +08:00
ChiGao
d96f264de7
feat(telemetry): link daemon HTTP request spans to inbound W3C traceparent (#9391)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(telemetry): link daemon HTTP request spans to inbound W3C traceparent

The daemon HTTP surface records a request span per request, but every span
starts a new trace: a caller forwarding the standard W3C traceparent header
(OTel-instrumented clients, proxies, gateways) gets no linkage back to its
own trace.

Extract traceparent/tracestate from inbound request headers in the daemon
telemetry middleware and parent the request span to that remote context.
Extraction reuses the same path as the existing JSON-RPC _meta extraction
(global propagator first, strict manual fallback so behavior is identical
without a registered SDK) and fails closed: requests without a valid header
keep the exact current span shape.

* fix(telemetry): guard inbound traceparent sampling and align W3C fallback

- Force TraceFlags.SAMPLED on inbound HTTP parents via the existing
  shouldForceSampled() matrix: an unsampled remote parent under the
  default parentbased_always_on sampler silently dropped the request
  span, the whole next() subtree, and the session-subprocess spans
  forwarded via _meta (review C1).
- Replace the hand-rolled manual fallback parser with a direct
  W3CTraceContextPropagator instance so acceptance rules (future
  versions, tracestate, all-zero ids, version-00 extension field)
  match the registered path with or without an initialized SDK.
- Gate middleware extraction behind isTelemetrySdkInitialized() to
  skip the hot-path parse when telemetry is off, and emit a debug
  daemon log when a present-but-invalid traceparent header is
  rejected.
- Re-export DaemonRequestSpanOptions from the core barrel and add a
  type-level guard so the parentContext field cannot silently
  disappear (vitest alone cannot catch its removal).

* chore(vscode): regenerate companion NOTICES.txt for @opentelemetry/core

* fix(telemetry): lazy-load OTel core fallback propagator behind SDK init

Address review feedback on the inbound traceparent linkage:

- Keep @opentelemetry/core out of the static graph. The module-level
  W3CTraceContextPropagator in daemon-tracing.ts pulled the CJS barrel
  (bot-measured +65,046 bytes) into every closure loading that module,
  including telemetry-off deployments. daemon-tracing.ts now keeps only a
  holder + setter (setDaemonFallbackPropagator, typed against
  @opentelemetry/api — type imports stay free at runtime); the lazy
  sdk-impl.ts chunk, whose closure already contains @opentelemetry/core via
  sdk-node/resources, constructs and injects the W3C instance on the
  successful SDK assembly path. Until injection, extraction returns no
  parent context: the HTTP edge is already gated on
  isTelemetrySdkInitialized (nothing changes when telemetry is off), and
  the _meta edge's consumers (withDaemonSpan / withInteractionSpan)
  short-circuit on the same flag, so an unresolved pre-init parent never
  had an observable effect.
- Add the mutation-verified fail-closed test for the header-extraction
  try/catch in daemonTelemetryMiddleware: a throwing extractor leaves the
  request settling normally (recordDaemonHttpRequest still fires once)
  with no parentContext on the span options.
- Record the rejected traceparent value (truncated to 128 chars) as
  http.request.header.traceparent on the invalid-header breadcrumb —
  traceparent only carries trace-id/span-id/flags, so this is
  privacy-safe and makes broken cross-service joins diagnosable.

Also document why the _meta extraction path deliberately skips
shouldForceSampled (trusted in-process bridge vs external HTTP input).

* feat(telemetry): carry inbound trace id into daemon access log with telemetry off

Telemetry off (the default) left daemon logs without any trace id: with no
request span, the log trace prefix never fires, so a caller forwarding W3C
traceparent could not be joined to its daemon log lines.

The middleware now parses the header with a plain regex
(extractInboundTraceId — same shape/all-zero/ff rejections as the W3C
propagator, no OTel machinery) and stores the trace id on the per-response
telemetry context. The access log emits it as the camelCase traceId field
of "request completed", keeping the log-based join alive with no telemetry
config and no trace backend. With telemetry on nothing changes: the request
span already carries the caller's trace id into the log prefix.

* fix(telemetry): unify _meta/HTTP sampling and repair build export

- Export extractInboundTraceId from the core barrel: the previous commit
  exported it from daemon-tracing.ts only, so downstream package builds
  failed with TS2305.
- extractDaemonTraceContext now applies the same shouldForceSampled()
  matrix as the HTTP edge: the _meta path is also reachable from direct
  ACP clients (acpAgent newSession/loadSession/unstable_resumeSession
  and Session.prompt pass caller-controlled _meta), so an external
  sampled=0 parent no longer silences daemon spans there either. The
  in-process bridge is unaffected (its injected values are already
  SAMPLED).
- The rejected-header breadcrumb now goes through sanitizeLogText so a
  crafted traceparent cannot forge log line structure with control
  characters.
- Add the sdk-impl wiring test: after initializeTelemetry the injected
  W3C fallback propagator resolves inbound HTTP parents.

* fix(telemetry): align log-path traceparent parsing and emit traceId in both modes

- extractInboundTraceId now mirrors the vendored W3C propagator's
  acceptance exactly: single optional leading/trailing whitespace and
  trailing extension fields above version 00 (version 00 must stay
  four fields). Previously the strict four-field anchor made the two
  paths disagree on the same forward-compatible header, silently
  dropping the access-log traceId for exactly the callers the
  propagator path supports.
- The camelCase traceId access-log field is now captured whenever a
  valid header parses, regardless of telemetry mode, so one saved log
  query / alert shape works for every deployment; with telemetry on the
  snake_case span prefix carries the same id redundantly.

* fix(telemetry): move inbound trace id getter out of the middleware module

52d572c0f2 made the access log statically import the telemetry
middleware module to read the captured inbound trace id. The access log
sits inside the serve fast-path pre-listen closure (run-qwen-serve
imports it directly), so the middleware's core-barrel import graph came
along for the ride and check-serve-fast-path-bundle started failing:
the 5.6MB core chunk (shell tool, glob, chokidar, @iarna/toml, fzf)
became statically reachable from run-qwen-serve.

Move the response-context symbol, its type, and the
getDaemonTelemetryInboundTraceId getter into a new import-light
telemetry-context.ts; the middleware imports the symbol from there and
re-exports the getter, so the access log no longer links against the
telemetry module at all.

* fix(telemetry): capture inbound trace id pre-auth under a dedicated symbol

* test(telemetry): pin the trace id seam through the context module getter

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-08-19 12:21:49 +00:00
samuelhsin
fd85efe474
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 17:46:43 +08:00
callmeYe
e4f5504e9f
feat(extensions): support authenticated HTTPS Git installs (#9458)
* feat(extensions): support authenticated HTTPS Git installs

* test(serve): update capability integration baseline
2026-08-19 09:06:54 +00:00
samuelhsin
83ad1eec30
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 16:10:17 +08:00
jinye
577f719130
fix(cli): surface daemon duplicate tool-call breaker as loop-detected stop (#9435)
The ACP daemon Session was the only duplicate-provider-id circuit
breaker path (PR #5657) that terminated silently: the turn ended as a
normal end_turn with nothing in the transcript and no telemetry, so the
session looked hung. Route the breaker through
recordDaemonLoopDetected with LoopType.GLOBAL_TOOL_CALL_DUPLICATE — the
same loop type the non-interactive CLI reports — so foreground turns
fail with the visible LOOP_DETECTED turn error, the context message is
preserved for the next turn, and the LoopDetectedEvent telemetry is
emitted. The bespoke repeatedDuplicateProviderToolCall result flag and
its dead consumer branch are removed in favor of the existing
loopDetected plumbing.
2026-08-19 07:14:27 +00:00
callmeYe
daa7d61990
feat(daemon): add batch extension activation APIs (#8788)
* feat(daemon): add batch extension activation APIs

* fix(daemon): focus extension batch API on V2

* fix(sdk): export extension batch types

* fix(core): reject empty extension batches

* test(extensions): strengthen batch regression fences

* feat(extensions): allow batch activation declarations

* fix(extensions): preserve legacy activation declarations

* fix(extensions): reject ambiguous legacy identities

* fix(extensions): preserve batch activation lifecycle

* feat(extensions): key batch activation by name

* fix(extensions): harden batch activation lifecycle

* fix(extensions): preserve declared activation lifecycle

* fix(extensions): reconcile renamed and legacy policies

* fix(extensions): preserve renamed artifact lifecycle

* fix(extensions): validate persisted artifact paths

* fix(extensions): preserve re-keyed artifact directory
2026-08-19 06:42:48 +00:00
samuelhsin
fdecc02830
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 13:45:54 +08:00
Shaojin Wen
1eb8a0c7f8
feat(review): wire --resume through /review and the review run subcommand (#9153)
Surface the local resume feature (PR #9092) on the paths a user reaches
it from:

- `parse-args.ts`: `/review <pr> --resume` parses to
  `resume: { requested, effective }`, gated on PR targets (a local
  review's diff comes from a live working tree with no stable interrupted
  state). A `--resume` on a non-PR target warns and is inert.
- `run.ts`: the `qwen review run` headless wrapper takes `--resume` and
  passes it through to the `/review` prompt.
- `SKILL.md` Step 1 gains a "Resuming an interrupted run" branch: on
  `resume.effective`, append `--resume` to `fetch-pr`, branch on its
  `resumed` JSON, run `recover-findings`, re-enter the audit loop at
  `latestReverseAuditRound + 1`, and read the restart bound back from
  `restartsSpent`.
- `DESIGN.md` / `docs`: document resume as a LOCAL convenience.

The CI review workflow runs FRESH — it does not pass `--resume`. A CI
attempt runs no-sandbox on the reviewed PR's own code and its worktree is
deleted the moment it exits, so there is no interrupted state on disk for
a retry to continue; a resume would refuse `worktree-gone` and start over
anyway. The retry loop and its test assert the fresh-only wiring.
2026-08-19 05:13:09 +00:00
jinye
83fc634f61
feat(serve): measure ACP child peak old-generation heap (#9380)
* feat(serve): measure ACP child peak old-generation heap

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): Omit unmeasured child heap reports and pin the GC observer path

Self-review round 1 on the child heap measurement:

- A child whose every getHeapSpaceStatistics() call throws (restricted
  container) reported a zeroed heap object with unclassifiedSpaceNames: [],
  which downstream reads as measured, needs-nothing, and coverage-complete.
  The probe now reports no heap at all until its first successful space
  read, matching what a child without the probe already sends.
- The GC observer callback — the only writer of peakLiveSetBytes,
  majorGcCount, and majorGcMs — had no test delivering a gc entry, so a
  wrong detail.kind check or a callback that never runs stayed green. The
  observer is now injectable and a test pins the major/minor split.
- The status/protocol/design text said the aggregate maximum "names the
  single worst-off child", but the aggregation takes Math.max per field
  independently. Wording now says each field is an independent maximum.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 05:00:07 +00:00
samuelhsin
37cf8be24c
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 12:45:42 +08:00
ytahdn
3192323c1d
perf(web-shell): keep streaming output responsive (#9405)
* perf(web-shell): keep streaming output responsive

* fix(web-shell): address streaming performance review

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-19 03:54:00 +00:00
jinye
dd82ba404e
feat(serve): Add live-state session activity watermark (#9396)
* docs(serve): Design live-state session activity timestamps

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(serve): Clarify live-state timestamp semantics

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(serve): Add live-state session activity watermark

Advance a bridge-local per-session activity watermark once when a prompt
that reached the running state publishes its formal terminal, project it
as the existing optional BridgeSessionSummary.updatedAt, and expose it on
the workspace live-state route. The advance is written before the
terminal is published so a client that observes the terminal cannot read
a stale value, and the extra millisecond keeps the watermark strictly
increasing when several terminals share a wall-clock millisecond or the
clock moves backward. A queued-only terminal, heartbeat, attach/detach,
or streamed update never advances it, and turn activity does not change
the session catalog version.

Populating the already-typed summary field lets full workspace session
lists merge live and persisted timestamps. Because the mtime and the
running-turn watermark are different authorities and the recorder writes
asynchronously, the merge picks the later valid timestamp instead of
blindly preferring the live value, so a row cannot move backward when an
async transcript write lands after the terminal.

Extend the response schema documentation for the live-state route and
GET /session/:id/status, and add the optional field on the TypeScript
SDK DaemonSessionLiveState type so consumers can pre-flight the tag once
and read the recency directly.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): Apply the later-valid activity rule on Live Task read paths

read_thread and wait_threads read the bridge summary directly, so once the
summary began carrying the running-turn watermark their fallbacks stopped
consulting the persisted transcript timestamp. Because the recorder writes
asynchronously, one task could report a later recency from the thread list than
from a thread read, and a wait cursor keyed on the live value alone stopped
changing when only the transcript advanced, so a consumer waiting for that
flush saw an unchanged cursor and exited early. Move the merge rule into a
shared helper and apply it at all three read points, including the revision
fallback used for a session with no attached client.

Add the watermark cases the design doc enumerates but the previous commit did
not ship: the deadline path publishes its terminal twice and must still advance
exactly once, a corrected forward clock jump must never decrease the value, and
a clock that advances between terminals must be reported instead of the logical
tie-breaker. Cover the single-session status route's verbatim pass-through of
the field, and cover the helper's both-invalid tail directly because no route
can supply two invalid candidates.

Correct two design-doc test-plan claims that did not match the code: the
teardown paths advance a watermark no consumer can read, because the entry
leaves live state in the same operation, and the duplicate deadline terminal
comes from the raced rejection reaching the settle handler rather than from a
late agent result.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): Merge live state on every organized session page

The organized view applied the live merge only on the first page, so page 1
sorted rows and encoded its cursor from merged activity keys while later pages
keyed the same rows by persisted mtime alone. That was harmless while bridge
summaries never carried an activity timestamp, because both keys were the mtime.
Now that a settled turn advances a watermark that leads storage until the
recorder flushes, a live row ordered onto page 1 by its watermark falls behind
the page-1 cursor boundary on page 2 and is returned a second time, displacing a
genuinely new row. Merge live state on every page so both pages key rows the
same way; a live-only row still has no persisted key to page by and stays a
first-page insertion.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(serve): Document activity-cursor duplication on live retirement

The activity key merges the live watermark, which is in-memory only, so a
session whose live entry retires mid-pagination falls back to its transcript
mtime and can be admitted again by a cursor encoded from the higher watermark.
Pre-change keys came from mtime alone and only advanced, so pages could skip a
row but never repeat one. Name that mode in the design doc and warn SDK
consumers of the activity-ordered cursors to key accumulated pages by
sessionId.

* fix(serve): Exclude emitted identities from activity-cursor re-admission

An activity key merges the bridge's in-memory watermark, so it is not a
stable property of a row: when a live entry retires mid-pagination the key
regresses to the transcript mtime, and a live-only row that persists mid-pass
re-enters the scan keyed by its first flush. Either way a row already emitted
on an earlier page could pass the strictly-older cursor filter again and
displace a genuinely new row, which was structurally impossible while activity
keys came from mtime alone.

The organized and metadata activity cursors now carry the identities already
emitted at a live-derived key, and the after-cursor filter excludes them, so
one pass returns a session at most once. The list prunes itself: an identity
is dropped once its persisted floor alone can no longer pass the key filter or
once the row leaves the filtered collection while not live. Past a 64-identity
cap the highest floors are dropped first, degrading to the previous at-most-
once duplicate instead of failing the pass. Cursors minted before the field
existed stay valid, and the field is omitted when empty.

* fix(serve): Close carried-identity drop paths in activity-cursor pagination

The emitted-identity carry could still drop a carried session mid-pass and
re-admit it later: an identity absent from a page's collection was discarded
even though absence can be transient (pre-flush TTL cache, mid-pass group
movement), organized re-entry was evaluated under the row's current pin state
only, and the live-only cursor key could move backward when a wall-clock
rollback landed the first watermark behind createdAt.

Retain absent carried identities at a negative-infinity floor, test organized
re-entry under both pin states, and floor the first watermark advance at the
entry's createdAt. Extend the retire test to a three-page pass so carried-set
propagation through an intermediate cursor is pinned, probe scan visibility in
the mid-pass-flush tests, and cover the live-list-failure and unpin paths.
Scope the at-most-once pagination wording in the design and protocol docs to
what the carry actually guarantees.

* docs(design): floor the first watermark advance at createdAt in the normative formula

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-19 03:31:44 +00:00
samuelhsin
7cd5040eed
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-19 10:50:58 +08:00
Shaojin Wen
846fc05461
feat(ci): post autofix failure-path handoff comments bilingually (#9386)
* docs(autofix): design bilingual failure-path handoff comments

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(ci): post autofix failure-path handoff comments bilingually

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): pin bilingual handoff sanitization by content, escape withdraw excerpt

Address review R1-1/R1-2/R1-3:

- Escape + iconv the issue-lane withdraw failure.md excerpt, the one
  publish site without `<!--` escaping: a failure.md quoting an HTML
  comment whose closer sits past the 1500-byte cut opened an
  unterminated comment that swallowed the new 中文说明 block (R1-3).
- Widen the escape-site count test to the multi-`-e` sed form
  (9 -> 12 sites) and pin the full zh sanitization pipeline per-site
  on both lanes; dropping the `<!--` expression from either zh site
  or a tag substitution from the withdraw site now fails (R1-1).
- Pin EN/ZH correspondence for every non-empty assignment site of
  HEADLINE/CAUSE/LAST_FIX/GATE_CLAUSE/IDLE_CLAUSE/REMEDY: count-only
  pins let a swapped adjacent HEADLINE_ZH pair pass all tests (R1-2).

All four mutation witnesses from the review now fail the suite
(verified locally: probe each mutation, expect red, restore).

* fix(ci): close the bilingual handoff review gaps (R2/R3)

Workflow fixes:
- Neutralize :: in the issue-lane run-log dump loop (agent-written
  files on step stdout parse as workflow commands; the PR-lane twin
  already did this) — R2-1.
- Extend the wrapper-defense substitutions (<details, </details,
  <summary) to the three excerpt sites that only escaped <!-- and
  now sit above the new 中文说明 wrapper: API_ERROR_DETAIL (flows into
  HEADLINE_ZH inside the wrapper) — R2-3; the PR-lane DETAIL_FILE
  excerpt (address-summary/no-action files are mandated to END with
  their own <details> tail, so a cut-straddling tail leaves a live
  severed opener) — R2-4; the withdraw failure.md excerpt — R3-1.
- The withdraw comment's 中文说明 block now renders unconditionally
  with a translated REASON (REASON_ZH per branch), mirroring the
  PR-lane headline floor: crash shapes where run-agent.mjs writes
  failure.md itself no longer degrade to zero Chinese — R3-2.

Accepted and documented (design doc §5): fence-token severance across
the byte cut — render-only, markers parse raw, and a balancing
heuristic stays wrong when the cut lands mid-closer — R2-2.

Test pins (each mutation-verified locally): branch-selected zh labels
— R2-5; zh gate-note text + condition + position — R2-6; failure.zh.md
membership in all four dump loops plus the issue-lane :: sed — R2-8;
the ZH_DETAIL guard — R2-9; full-line rm -f pins on the three pre-agent
cleanup sites — R2-10; the BODY append shape — R3-3; wrapper internal
ordering — R3-4. Design doc §2 reconciled with §5 on the no-detail
fallback sentence — R2-7.

* fix(ci): close the R4 review gaps (case-insensitive tag defense, pin gaps)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-19 00:17:35 +00:00
samuelhsin
4c392a67e5
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-18 22:50:28 +08:00
Shaojin Wen
081a96d864
feat(cli): plain-prose /review comments; severity markers follow review.attribution (#9027)
* feat(cli): Add review settings for attribution, default effort, and default comment

* fix(cli): resolve review settings from operator scopes and close gate gaps (#8994)

Address review feedback on the review settings:

- Resolve review.attribution/effort/comment from operator-controlled
  settings scopes only (system defaults, user, system); a repository's
  .qwen/settings.json is content under review and must not control
  whether findings publish, whether the review names its model, or how
  deeply the pipeline verifies.
- Normalize the configured review.effort through the same case-
  insensitive validation as the --effort flag, so "Low" cannot miss the
  exact comparisons the forcings run and invalid values cannot leak into
  the verdict.
- Gate the modelId requirement and footer-safety validation on
  attribution: with the footer gated off, the field has no consumer and
  must not refuse the run.
- Pass the standing review.comment setting into publish-assets' call of
  the shared authorisation gate, so both callers agree on what
  authorises a run.
- Make presubmit's self-comment detection footer-independent by also
  matching the reviewing account's own top-level comments, so
  attribution-off posts still dedup.
- Align SKILL.md's Step 7 gate and every --comment branch on
  comment.effective, and add handler-level wiring tests for all
  configured defaults.

* test(cli): pin the review-settings operator defaults with unit tests (#8994)

* fix(cli): share the guarded footer strip and pin the gate audit text (#8994)

* fix(cli): raise the repository-context array bound to 256 (#8994)

* fix(cli): validate review setting values and tighten the review gates (#8994)

* feat(cli): drop the AI template tells from unattributed /review posts

review.attribution: false already drops the footer; the posted text still
read as machine output. With attribution off, inline comments now post
without the **[Critical]**/**[Suggestion]** prefixes and are written as
plain reviewer prose, the review body loses its fixed template markers
(LGTM! , the ⚠️ glyph, the **[Critical]** bullets in body lists), and
the Step 1 verdict carries the attribution flag so the orchestrator can
pick its register. The severity strip happens in the final post object
only — counting, the unmarked gate, and the ledger all still run on the
marked payload, so verdict semantics are unchanged and the default mode
is byte-identical to before.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): align presubmit dedup with severityOf and normalize auto effort (#8994)

* feat(cli): make plain prose the only register for posted /review text

The template voice is worse prose for every audience, not just the
unattributed one, so the phrasing now goes plain unconditionally: comment
bodies drop the '— Failure scenario: trigger → outcome' label and arrow
notation (the evidence rule is unchanged — trigger and wrong outcome must
be in the sentences), and the fixed review-body copy loses LGTM!  and
the ⚠️ glyph in both modes. What still follows review.attribution is the
machine-readable layer — the severity prefixes and the footer — because
qwen-autofix.yml's Critical-only mode greps posted bodies for the literal
**[Critical]** marker. With prose unconditional there is no register to
branch on, so the parse-args verdict's attribution field goes away again;
submit keeps stripping prefix and footer at post time when the operator
turned attribution off.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): keep the copy humans actually write — restore LGTM! and the warning glyph

Readability, not concealment, is the criterion: reviewers type LGTM! and
reach for the ⚠️ glyph on a warning line every day, and both aid scanning.
The earlier commits stripped them along with the real scaffolding, which
overshot. What goes is only the labelled failure-scenario template; the
fixed review-body copy is now byte-identical to before in both attribution
modes, and the pr-context LGTM filter needs no change after all.

* fix(cli): close the review-found gaps in the unattributed post path

CI review on the PR found three real defects and four test/doc gaps in the
first two commits; all addressed:

- presubmit dedup went blind to attribution-off posts: the authorship
  fallback gated on severityOf, but submit strips exactly that prefix, so a
  later round re-posted its own findings as duplicates. Attribution-off
  comments now carry an invisible <!-- qwen-review --> marker and presubmit
  matches on it — from any account, which also closes 8994's documented
  other-accounts gap.
- The attribution-off body-Critical branch quoted entries verbatim, leaking
  a model-written **[Critical]** marker into the posted body; it now strips
  like the inline path does.
- The SKILL.md payload example still showed the labelled template the
  rewritten body-format paragraph forbids; both examples now show plain
  prose.
- A comment that is nothing but its severity marker is refused at the
  consistency gate instead of posting the bare marker.
- Forged footers followed by text survived the trailing-anchored strip and
  were the only attribution an unattributed post carried; the off leg now
  strips footer-shaped lines regardless of position.
- The setting's description now names both stripping consequences (autofix
  Critical-only deferral; the invisible marker), and the loosened
  clean-approve test assertions are exact again.

* test(cli): pin the reply guard with a finding-shaped reply fixture

The unmarked reply body was excluded by the severityOf gate even with the
reply guard deleted, so nothing pinned the guard itself (mutation-verified
by CI review).

* fix(cli): close the round-3 review findings on the unattributed post path

Five Criticals and twelve Suggestions from the third CI review round, all
addressed:

- presubmit dedup: the invisible-marker branch was ungated — any account
  could plant the public marker string on a line expecting a blocker and
  have the next round silently withhold it. The branch now requires
  authorship by the reviewing account plus the exact trailing shape submit
  posts; adversarial and quote-reply fixtures pin both guards. The
  'other accounts escape dedup' disclosure from 8994 stands again.
- The marker-only gate was dead under attribution ON (the canonical footer
  was appended before the check) and stacked markers bypassed it: the
  strip is now iterative, delegates the classification to severityOf, and
  the gate refuses when the footer-and-marker-stripped remainder is empty
  or still marker-led.
- bodyCriticals and cannot-tell entries now get the forged-footer strip on
  the unattributed leg (a surviving mid-entry footer was the post's only
  attribution), the cannot-tell parse trims before matching, and ledger
  titles strip the marker (the ledger rides the body as an HTML comment
  the autofix grep reads).
- stripForgedFooterLines rewritten line-based: closing underscore optional
  (looping-model truncation), CRLF tolerated, 400-char line bound, fence-
  and indented-code aware, and byte-identical when nothing matches.
- The comment marker now carries severity (<!-- qwen-review critical -->);
  pr-context's blocker promotion reads it, so an unresolved unattributed
  Critical re-enters the re-check section every round — including past the
  ledger's horizon.
- Tests: stripForgedFooterLines unit coverage, grouped cannot-tell strip,
  ledger leg under a prNumber plan, and the adversarial presubmit shapes;
  loosened assertions re-tightened. Docs and the settings description now
  match the shipped behavior.

* fix(cli): close the round-4 review findings — marker read/write hardening

Seven Criticals and four Suggestions from the fourth CI review round:

- commentMarkerSeverity now reads only the trailing posted shape, and
  submit strips pre-existing bare marker lines before appending the
  canonical marker — a marker string quoted or planted in a reviewed file
  can no longer choose the severity the classifiers see.
- The marker disjunct in the blocker classification is gated on the
  reviewing account, via one shared predicate (isBlockerBody) now used by
  BOTH pr-context and comment-status — an empty planted 'critical' comment
  no longer becomes a permanent irrefutable blocker, and the two consumers
  can no longer diverge on the posted shape.
- The ledger's drafted-comments leg strips like the bodyCriticals leg
  (iterative markers, forged footer lines first, footer spans off the
  title), and stripSeverityPrefix now strips to empty for marker-only
  bodies — the submit gate refuses exactly that shape, in both modes.
- The fence scanner is a faithful model now: ~~~ fences count, a fence
  opener indented 4+ spaces does not open one, and lines inside a simple
  HTML block never toggle fence state.
- Producer/consumer roundtrip tests pin the marker shape (the drift class
  the module header exists to prevent); the iterative strip, the
  attribution-on marker-only gate, and the strip order each carry the
  assertion the mutations showed missing.

* fix(cli): make the unattributed strip a fixpoint, closing the round-5 escapes

Seven Criticals from the fifth CI review round, all probe-verified escape
hatches in the strip chain, closed by restructuring it:

- One shared stripForUnattributedPost iterated to a fixpoint now serves
  every attribution-off leg (submit's post transform and gate, compose's
  body lists, both ledger legs), so the sites cannot drift on order:
  forged footer lines, severity prefixes (leading AND paragraph-initial,
  via a new fence-aware stripParagraphMarkers), bare marker lines, and
  footer spans interleave arbitrarily in a looping draft and only the
  fixpoint posts none of them.
- The marker-only gate runs the full chain: a prefix over a bare marker
  line no longer posts an empty visible comment carrying a live marker.
- Marker-only body Criticals and cannot-tell entries are refused at
  compose (both modes), mirroring submit's gate — an empty-stripped entry
  no longer counts toward REQUEST_CHANGES while rendering nothing.
- The version-parens truncation (the natural mid-character cut) is
  admitted by all three footer regexes; blockquoted forged footer lines
  strip; HTML blocks stop shielding footer lines (their content renders
  visibly) while still not toggling fence state.
- The design doc's definitional line now says what ships: no VISIBLE
  attribution — the machine contract moves to the invisible severity
  marker.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): drop the dead bare-marker arm and tolerate whitespace before the colon

- submit's post transform no longer references COMMENT_MARKER: the
  unmarked gate runs first, so every body reaching the transform has a
  known severity and posts the severity-carrying marker unconditionally.
- stripSeverityPrefix tolerates whitespace before the colon after a
  marker and treats a whitespace-only remainder as empty (a trailing
  newline no longer survives as a phantom non-empty body).

* fix(cli): close the round-6 findings — faithful fences, render-nothing gates

Seven Criticals from the sixth CI review round:

- mapLinesAware now applies the map inside HTML blocks (the round-5 fix
  updated the comment but not the code — the shield stood), tracks the
  opening fence by delimiter character and run length with no info string
  on the closer (CommonMark), and the chain's final span pass is line-aware
  so fenced quotations survive the full strip.
- The emptiness gates (submit + both compose lists) project through a new
  rendersAsNothing — Cf characters, HTML comments, hollowed fences, and
  forged-footer residue are not content — and run the full post-transform
  chain, so a scaffolded-but-invisible comment can no longer post, count
  toward REQUEST_CHANGES, and re-promote as an unanswerable blocker.
- stripCommentMarkerLines admits blockquoted marker lines, matching its
  sibling regexes.
- buildLedger keeps the carried id and title when the finding text starts
  on the line after the severity marker (trimStart before titleOf) — a
  regression from routing titles through the new chain.

* fix(cli): close the round-7 findings — faithful quotes, code spans, render-nothing classes

* fix(cli): close the round-8 findings — one displayed projection for every strip

* fix(cli): close the round-9 findings — fail-closed identity, one shape per leg

* fix(cli): close the round-10 findings — rendered-text signals, bounded spans, fence-safe entries

- blocker prose scan reads only rendered text: an HTML comment renders as
  nothing, so a planted `<!-- [critical] -->` can no longer promote an
  invisible, irrefutable blocker through the ungated channel
- the footer-span version group admits only the version shape
  footerVersion validates — a span truncated inside the parens can no
  longer swallow the prose after it
- the marker strips admit the full-width colon, closing the marker-only
  refusal's ASCII-only hole in bilingual drafts
- entries containing a code-fence delimiter line are refused for redraft:
  the one-line collapse turns them into an unclosed fence that swallows
  the rest of the posted body
- the identity fail-closed trigger narrows to what identity actually
  gates — critical markers on root comments — so a planted reply cannot
  convert a transient identity blip into a repeating refusal
- the ledger's carried-id anchor reads through render-nothing residue
  left between the marker and the id, ending the silent renumbering
- the marker-only contract covers trailing Cf/comment residue

* fix(cli): close the round-12 Criticals — bounded footer version, drop-scoped blank cleanup, quote-preserving markers, gate-matched post leg

* fix(cli): close seven review sanitation entrances from round-20 review (#9027)

- CR-aware line model: scanLines and rendersAsNothing split on CR/CRLF;
  entry lists normalize line endings on ingest, so a bare CR can no
  longer hide a forged footer, a hollow fence, or a fence delimiter
  from the refusal and emptiness gates (R20-1)
- empty-login identity lookups fail closed like thrown ones in both
  pr-context and comment-status while a critical marker is posted (R20-2)
- whitespace-only body-list entries fail the renders-nothing gates
  instead of vanishing before them; the dead raw sha check drops (R20-4)
- drop-collapse never touches blank runs around an HTML-block content
  drop — quotation blanks render and survive (R20-6)
- attribution-off posts refuse drafts whose post-strip shape leaves a
  fence open at the appended invisible marker (R20-9)
- carriedClaimLine slices on the classifier's projection and both
  colon widths; presubmit reads carried ids off the attribution-off
  posted shape (R18-1)
- duplicates disclosure routes through the attribution-off fixpoint
  chain like every other body leg (R15-1)

---------

Co-authored-by: qwen-code-autofix[bot] <qwen-code-autofix[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-18 14:47:42 +00:00
samuelhsin
4d352542ed
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-18 20:12:18 +08:00
ytahdn
0c35b304a3
fix(web-shell): use backend-authoritative queue state (#9407)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-18 12:07:55 +00:00
samuelhsin
6ee4071168
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-18 17:31:16 +08:00
Shaojin Wen
da26cffc36
feat(review): Aone Code read path (second review-platform provider) (#9226)
* docs(design): /review Aone Code read path (Phase 2)

* feat(review): Aone Code read path (Phase 2)

Adds an Aone Code provider so /review can review a MaxCompute CR locally.
The read path works end to end against a real odps_src CR (verified E2E):
fetch-pr fetches `refs/merge-requests/<global-id>/head` and builds the
worktree + diff (stats computed locally, Aone advertises none); meta /
issue-context / fetch-diff resolve identity, Aone workitem evidence, and the
diff via the a1 CLI. The four reader-backed subcommands and fetch-pr select
the provider from the clone's remote (or an Aone host), so a GitHub clone is
unchanged.

Read-only this phase: pr-context / comment-status / presubmit have no Aone
backing yet (the run degrades to context-unavailable), and --comment is
refused on an Aone target. SKILL.md + code-review.md document the Aone
target and the degradations. See docs/design/2026-08-15-review-aone-provider.md.

* fix(review): address PR #9226 round-1 Aone review findings

Critical:
- match-remote: Aone CR URLs use the WEB host (code.alibaba-inc.com) while
  a clone's remote uses the GIT host (gitlab.alibaba-inc.com) — treat them as
  one equivalence class (hostsEquivalent) so a codereview URL matches its
  clone's remote and the worktree flow is reachable; nested-group remotes
  (group/subgroup/project) now collapse to the last two segments instead of
  failing to match
- registry: an explicit non-Aone host/remote now beats the cwd probe, so an
  explicitly-GitHub subcommand run from an Aone clone is not hijacked to
  Aone; hint host is trimmed; the four reader-backed subcommands thread
  --host into detection (previously dropped); dropped the unwired --platform
  dead switch
- aone parseRemoteUrl: user-less scp remotes (ssh-config/insteadOf), nested
  groups, a trailing slash after .git, and empty segments now parse; the
  parse-failure message redacts a user:token@ origin (no credential leak)
- aone getCommentBody throws on a missing id (was an indistinguishable empty
  string); fetchDiff uses gitRaw (512 MiB buffer, no CRLF rewrite, latin1)
  instead of git() (1 MiB ENOBUFS, CJK byte loss)

Suggestions:
- aone-client: 120 s timeout, ENOENT branch in the auth check ("install the
  a1 CLI"), TRANSIENT_RE anchored to HTTP 5xx (bare 502/503/504 misfired on
  command lines containing those digits), one stderr trace line per retry
- fetch-pr: validate pr_number before Number() coercion (1e3 fetched PR
  1000), trim --host before detection; submit guard trims --host;
  comment-body --pr help notes the Aone per-MR requirement
- parse-args: nested-group codereview URL grammar; invalid-url warning names
  both grammars
- SKILL.md + code-review.md: Aone paragraph corrected (clone-origin trigger,
  Agent 0 skipped, test-plan/publish-assets unbacked, pass --host), design
  doc updated (detection, Agent 0 gating)
- tests: aone.test.ts, registry cwd-mock + precedence + parseRemoteUrl cases,
  remote-match hostsEquivalent + nested collapse, parse-args nested codereview,
  submit-aone refusal

* fix(review): address PR #9226 round-2 Aone review findings

Critical:
- parse-args: the Aone CR URL grammar is now constrained to Aone hosts
  (*.alibaba-inc.com) — a /codereview/ URL on any other host hits the
  fail-closed invalid-url refusal instead of becoming a live PR target
  (unlike …/pull/<n>, which any GHE host legitimately serves)
- aone fetchDiff: merge-bases against a fetched target branch (not a
  present-but-stale origin/<target>), and the MR-head refspec is now
  force-fetched (+) so a stale throwaway ref from an interrupted run does
  not fail the fetch when the head was rewritten (normal AGit-Flow iteration)

Suggestions:
- fetch-pr: countDiffChangedLines now delegates to the single hunk-state
  walker in computeDiffStats (the two could not disagree silently); the
  changedFiles count is pinned on `diff --git` via a binary-file fixture
- aone parseRemoteUrl scheme case made explicit + pinned (RFC 3986)
- submit.test.ts pins the platform registry to GitHub so the Aone refusal
  guard neither spawns a real git in the vitest cwd nor couples to the
  machine's clone origin

* fix(review): address PR #9226 round-3 Aone review findings

Critical:
- registry: hostOfRemoteUrl now makes `user@` optional in the scp branch
  (user-less scp remotes from ssh-config/insteadOf no longer misroute an
  Aone clone to GitHub); the token-bearing scp userinfo parses to the host,
  not an owner
- parse-args: the Aone CR-URL host group now requires a REAL subdomain dot
  boundary (`(?:[A-Za-z0-9-]+\.)+alibaba-inc.com`), so lookalikes
  (`evilalibaba-inc.com`) hit the fail-closed refusal; a `/pull/<n>` URL on
  an Aone host is refused too (Aone serves no /pull/ pages)
- meta: on a non-GitHub platform an explicit `--repo` without `--host` is
  refused (no default host off GitHub) instead of emitting the contradictory
  `platform:aone` + `host:github.com`
- aone fetchDiff: spreads PINNED_DIFF_CONFIG/PINNED_DIFF_FLAGS (an un-pinned
  color.diff=always zeroes computeDiffStats), discloses a failed target-branch
  fetch via a stderr WARNING, and refuses to diff from a clone of a different
  repo; scp-form userinfo is redacted in the parse-failure message

Suggestions handled:
- comment-body: the Aone per-MR `--pr` requirement is enforced before the
  auth gate (usage errors precede auth)
- aone-client: the auth-failure diagnostic surfaces a1's real first stderr
  line (not the execFileSync preamble) and reports a timeout/kill distinctly
- remote-match docstring + registry precedence comment updated to the
  implemented behavior

Deferred to follow-up #9194: the 16 test-gap patterns, headRefOid dead-field
removal, MAX_SAFE_INTEGER digit guard, and the refusal-message host branch.

* fix(review): address PR #9226 round-4 Aone review findings

Critical:
- aone fetchDiff + fetch-pr merge-base fetch: the server-controlled
  target/base branch reached `git fetch` bare — a dash-leading branch name
  (creatable by full-refname push) parses as an option, so
  `--upload-pack=<payload>` executed attacker-named code with the
  reviewer's credentials. Pass `--` to end option parsing and refuse
  dash-leading values outright on both providers
- meta: the no-default-host guard now gates on the FLAG, not the resolved
  value — a GH_HOST export no longer bypasses it (and an empty-string
  --host counts as missing); the whole --repo branch's pure resolution
  moves above the auth gate (usage errors precede auth)
- skill: pass --host for EVERY pr-url target including github.com — an
  omitted hint falls back to the cwd origin probe, which hijacked a
  github.com review run from an Aone clone (and vice versa); lightweight
  fetch-diff/pr-context carry the host too
- submit: the Aone refusal moves BELOW the authorisation gate and takes
  the exit-3 + {"posted": false} shape instead of throwing — an
  unauthorised Aone run now ends as the skill's contract defines, and
  detection reads the effective host (flag → GH_HOST), so an Aone-pointing
  GH_HOST export is refused instead of dying opaque inside gh

Suggestions handled:
- parseRemoteUrl: strip query/fragment (credential channel into repo
  identity), fix the cleaning order for two-plus trailing slashes after
  .git, and discard an explicit port instead of folding it into the path
- registry: isAoneHost normalizes the trailing-dot FQDN spelling; the cwd
  probe delegates to lib/git's gitOpt (shared git policy)
- aone: the MR-head refspec is stated once (mrHeadRefSpec); resolveRepo
  quotes git's real error line, not the execFileSync preamble
- aone-client: the auth fall-through message is neutral (covers
  non-auth failures the login hint cannot fix)
- fetch-pr: pr_number guard tightened to ^[1-9]\d*$ (no PR zero, no
  leading zeros, no side effects before the refusal)
- the five detection-consuming subcommands' --host describes now state
  the implemented semantics; SKILL.md/code-review.md read-only phrasing
  corrected and the false "detection reads the clone's remote, not the
  URL" claim fixed

Tests: dash-leading refusal on both providers, meta guard flip tests
(GH_HOST bypass, empty flag, pre-auth), submit exit-3 shape (authorised,
unauthorised, padded host, GH_HOST), trailing-dot and /pull/-on-Aone
parse refusals, port/query/slash parse cases, fetch-pr zero-number and
base-ref refusals.

Deferred to follow-up #9194: the single-branch merge-base disclosure
(R3-9), cleanup audit skip-in-code (R3-13), URL-form --remote hint
(R3-19), publish-assets refusal parity (R3-22), and the data-path
deadline translation (R3-25).

* chore(review): re-push to re-link PR head after branch recreation

* test(review): repin SKILL.md host-rule wording in SKILL.test.ts

The round-4 fix rewrote the skill's --host notes (pass --host for every
pr-url target, github.com included); three revert-guard tests pinned the
old 'add --host <host> for Enterprise' phrasing and reddened the core
suite in CI. Repin them at the new wording.

* fix(review): address PR #9226 round-5 Critical findings

- aone resolveRepo: redactUrl now strips the query/fragment channel too —
  a ?private_token=… origin carries no @ for the userinfo redaction, so the
  parse-refusal message echoed the secret the success path strips (test
  pins the refusal message secret-free)
- aone fetchDiff: the merge-base fallback (base = ref~1) DISCLOSES via a
  stderr WARNING — previously silent, a multi-commit MR got only its last
  commit served as the complete diff (shallow/single-branch clones hit
  this; the GitHub path is loud about the same class)
- submit: the Aone write-refusal binds the platform in BOTH directions —
  the authorisation gate now surfaces the recorded target's host, so a
  recorded Aone host refuses whatever the runtime-effective host resolves
  to (an ambient GH_HOST export can no longer steer an Aone review into
  posting at a same-named repo), while a recorded non-Aone pr-url binding
  is no longer vetoed by the cwd probe from an Aone-origin clone

Tests: refusal-message redaction, fallback disclosure (spy calls captured
before mockRestore — vitest's restore clears them), bidirectional refusal
arms (recorded-Aone + GHE env refuses; recorded-github + Aone cwd posts).

* fix(review): address PR #9226 round-6 Critical findings

- authorization: the --user-authorized fast path now surfaces the recorded
  target's host too (best-effort read of the recorded args) — it returned
  before the args file was read, so recordedHost was always undefined on
  that path and the 'a recorded Aone host always refuses' invariant leaked:
  a user-authorised post of a recorded Aone codereview review from a
  non-Aone cwd with no --host/GH_HOST posted at github.com's same-named
  repo. Tests pin the fast-path host through the REAL gate and the
  end-to-end refusal (the witness scenario)
- aone: the query/fragment strip now uses [\s\S]* in both redactUrl and
  parseRemoteUrl — git stores newline-bearing remote URLs, and a plain .
  stopped at the first \n, letting ?private_token=SECRET\nx smuggle the
  token past the strip into the parse-refusal message. Tests cover both
  the parse-success and refusal paths of the smuggle

Round-6 is Critical-only per the ~5-round policy (user-confirmed for
convergence); the 13 Suggestions are deferred to follow-up #9194.

* fix(review): address PR #9226 round-7 Critical findings

- fetchDiff's throwaway ref now carries a pid suffix — two concurrent runs
  for the same MR in one clone shared the name: one session's finally-
  delete killed the other mid-review (unknown revision), and a
  pre-existing local branch of the reserved name was force-moved then
  deleted, reflog and all (race probe: 12/60 failures → 0 with the
  per-run unique name)
- the target/base-ref guards close the refspec channel the dash-only
  check left open after `--`: a leading `+` parses as a force refspec
  (fetches the wrong head — stale evidence, no WARNING) and a colon as
  src:dst (force-moves the throwaway ref or a reviewer-local branch).
  Both providers now refuse '-', '+', and ':' shapes (probe-confirmed on
  real fetchDiff incl. the served-wrong-diff and local-branch-overwrite
  witnesses); tests pin the new channels on both guards
- redactUrl and parseRemoteUrl clean userinfo BEFORE the query/fragment
  strip: a userinfo that itself contains '?' or '#' was truncated
  mid-credential, leaking the username+secret prefix into the refusal
  message and making parseable origins unparseable (flip-verified on the
  witness shapes)

Round-7 is Critical-only per the convergence directive; the 8
Suggestions (incl. the 4 bot findings) are deferred to #9194.

* fix(review): address PR #9226 round-8 Critical findings

- the server-controlled branch-name guards now validate ALLOWLIST-style on
  both providers (aone.fetchDiff's target, fetch-pr's baseRefName): the
  denylist admitted HEAD (silent fetch + merge-base through the stale
  clone-time symref), rev-parse metasyntax (wrong base under a
  misdescribing warning), ranges, and the empty string (garbled diff-less
  fallback) — a plain-branch-name shape closes every channel
- parseRemoteUrl/redactUrl consume userinfo GREEDILY up to the last @ of
  the authority — multi-@ and :-/-bearing token userinfo no longer leaks
  cleartext residue through the refusal messages or folds into the parsed
  host (take() fails closed on any surviving @); the scp strip admits only
  a removal that leaves a host: shape behind
- fetch-pr's Aone stats backfill moves AFTER the plan/rescue, where
  diffText is final — the partition-rescue republishing the full range no
  longer leaves delta-scoped numbers beside a full-range diffPath — and
  isCollapsedFromUpstream is skipped when the stats are locally derived
  (one source, not two: the disclosure needs an independent advertised
  fact, and a delta-scoped round beside the full-range count fired a
  false collapse)
- remote identity is injective again: Aone nested-group targets carry the
  full group path (parse-args → match-remote --group-path → matchRemotes
  compares every segment when both sides have three or more), and
  fetchDiff's origin guard adds the origin's host (Aone family) — a
  same-named repo in another group or on another platform can no longer
  pass either gate; SKILL.md passes --group-path for nested targets
- meta's discovery branch drops GH_HOST inheritance off GitHub — an
  ambient GHE export beside an Aone-origin clone no longer vetoes the
  valid invocation at HOSTNAME_RE; only an explicit --host steers routing

Round-8 is Critical-only per the convergence directive; all five findings
fixed, no deferrals this round.

* fix(review): address PR #9226 round-9 Critical findings

- redactUrl is fail-closed BY CONSTRUCTION: split at the last @, redact
  everything before it — the per-regex redaction kept missing shapes
  (round-9: URL userinfo with a / in the secret, scp userinfo with a
  newline, residues with no host: shape all leaked verbatim through the
  parse-refusal message)
- parseRemoteUrl cleans per form and fails CLOSED: URL-form userinfo is
  bounded to the authority (greedy within it — multi-@ and ?/# inside
  secrets consumed whole, /-bearing secrets left to fail closed in take),
  scheme inputs never fall through to the scp grammar (a malformed
  https://user:pa/ss no longer parses host user); the round-8 scp-strip
  firing on scheme URLs fabricated coordinates from query-borne and
  path-borne @ witnesses — all witnesses now parse correctly or refuse
- registry hostOfRemoteUrl consumes token-bearing userinfo (':' AND '/'
  in the secret) on both branches, mirroring aone.parseRemoteUrl —
  detection no longer parses the credential prefix as the host and
  misroutes Aone clones to GitHub; detectPlatformKind ranks an explicit
  --host above the remote-URL hint in BOTH directions (an Aone origin
  can no longer hijack an explicitly-GitHub invocation into fetching a
  global MR id from the wrong remote)
- nested-group identity is injective in both directions: matchRemotes
  compares the full group path exactly whenever the target carries one
  (any length — a 3+-segment target no longer matches a two-segment
  remote sharing its tail, nor the reverse); Aone CR targets carry the
  path even at two segments and the canonicalized URL keeps the full
  path; fetchDiff's origin guard compares the origin's full path against
  the MR's own detailUrl path (authoritative repo identity, where the
  seam's ownerRepo is collapsed); the rescue pool keys on the full path
  and same-id cross-group CR URLs are refused as ambiguous

Round-9 is Critical-only per the convergence directive; the 8
Suggestions (R8-6..R8-13) are deferred to follow-up #9194.

* fix(review): address PR #9226 round-10 Critical findings

- aone.fetchDiff's host arm keys on the CANONICAL Aone-family predicate
  (new remote-match isAoneHostFamily: port/trailing-dot/case normalized;
  registry.isAoneHost now delegates to it) — a trailing-dot FQDN clone
  that detection accepts as Aone can no longer be refused by the diff gate
  with a misdirecting remedy
- the URL cleaning/redaction class is closed structurally, not per shape
  (sixth consecutive round a new entrance was found): parseRemoteUrl's
  URL-form userinfo is consumed whole WITHIN the authority (span between
  // and the first /), and the scp-form userinfo strip + its lookahead are
  bounded at ?/# — an @ inside a query or fragment value is the
  credential's own character and can no longer fabricate coordinates from
  the query tail; redactUrl fails the DISPLAY closed with a constant when
  the last @ sits after a ?/# marker — the token tail can no longer reach
  the refusal message (URL/scp/fragment witnesses all pinned)
- isPlainBranchName rejects git's pseudo-ref set (FETCH_HEAD/ORIG_HEAD/
  MERGE_HEAD/…) on both guards — FETCH_HEAD resolves to the just-fetched
  PR head (empty diff beside full-range metadata), ORIG_HEAD to an
  arbitrary ancestor; both shape-legal, both silently wrong
- fetch-pr's merge-base probe requires the fetch to have produced the
  tracking ref — a tag-only baseRefName exits 0 writing only FETCH_HEAD,
  and the bare-name fallback once merge-based against the reviewer's
  local tag with baseFetchFailed falsely false; the tag shape now lands
  in the disclosed state
- parse-args: the repo-qualified CR URL outranks a same-number bare
  spelling as the target in BOTH the rescue pool and positional order —
  the bare number carries no host, and letting it win flipped detection
  onto the cwd fallback (a loud refusal at the merge base had degraded to
  a silent wrong-platform retarget); bare restatements of the URL target
  are skipped silently, matching the rescue loop's restatement handling

Round-10 is Critical-only per the convergence directive (the bot's own
ledger is at its round cap); the 6 convergence-posture deferrals named in
the review body join follow-up #9194.

* fix(review): address PR #9226 round-11 Critical findings

- the URL cleaning/redaction surface is closed STRUCTURALLY: one parser,
  one source of truth — registry.hostOfRemoteUrl now delegates to the
  canonical aone.parseRemoteUrl (detection and the identity parser can no
  longer disagree), and the scp branch reads GIT'S OWN grammar
  (GIT_TRACE-probed: hostinfo ends at the FIRST ':', userinfo carries no
  ':' or '/') — the last-'@' consumption once parsed a different host than
  git connects to, letting fetchDiff's same-repo guard pass while git
  fetched from another server; token-bearing scp shapes now fail closed,
  and the round-8 detection tests are re-blessed onto shapes git reads
  that way
- the pseudo-ref allowlist is CASE-INSENSITIVE on both twins: on
  case-insensitive filesystems (macOS/Windows defaults) fetch_head folds
  onto FETCH_HEAD, resolving the merge-base to the just-fetched MR head
  (empty diff beside full-range metadata); lowercase spellings refused,
  pinned
- submit.test.ts's file-level setup now saves/clears/restores GH_HOST —
  the Aone refusal reads the ambient env, and the org's standard intranet
  export pattern (an Aone-family host) turned 50 of 69 posting tests into
  refusals
- the --user-authorized fast path binds the recorded host to THIS write
  (same-PR number only — a stale recording of another PR must not supply
  a host) and scans SIBLING session recordings when the session-scoped
  args file is absent — the characteristic cross-session publish shape
  otherwise lost the host and posted a recorded Aone review at
  github.com's same-named repo (real-gate witness: exit 0, COMMENT filed);
  tests drive the real gate through a sibling-session fixture
- the tracking-ref requirement and both merge-base sites are FULLY
  QUALIFIED (refs/remotes/…): git resolves unqualified origin/<name> in
  refs/tags and refs/heads first, so a tag or branch literally named
  origin/<baseRefName> — a PUSHABLE, server-controlled refname a plain
  clone auto-carries — shadowed the just-fetched tracking ref and moved
  the merge base with no disclosure; shadow-tag tests pinned on the
  resolveMergeBase probe, the fetch-pr seam, and aone.fetchDiff

Round-11 is Critical-only per the convergence directive; the bot's own
ledger is at its round cap and this round still produced findings —
recommend freezing the bot loop and moving to human security review.

* fix(review): address PR #9226 round-12 Critical findings

- the recorded-args host lookup is HARDENED — the store lives under
  .qwen/tmp/ beside review worktrees checked out from the PR's own tree,
  so its content is attacker-influenceable: only s-* session directories
  are scanned (a malicious PR can no longer plant a root-level args file
  that binds a host), symlinks are skipped at both the directory and file
  levels (mirroring writeSkillArgs' O_NOFOLLOW write-side policy), reads
  are size-bounded, and the host binds only when the recording names the
  same PR number AND the same repo
- the canonical Aone invocation shape (bare global MR id, no URL) can no
  longer post cross-session without host evidence: a same-number
  recording with no host binds the recorded --host flag when present
  (parse-args now records it), and without one the write gate FAILS
  CLOSED with the exit-3 shape and names the remedy — instead of posting
  the review at github.com's same-named repo (the probe-verified witness
  once exited 0 and POSTed)
- parse-args: the URL-outranks-bare-number invariant now holds for MIXED
  shapes — a positional bare number restating the rescue pool's single
  PR is carved out of hasValidCandidate, so --effort <cr-url> 7 (and
  both orderings/equals-form) target the CR URL instead of silently
  retargeting onto the cwd clone's same-number PR; a different number
  still outranks
- aoneReader.resolveRepo refuses an origin outside the Aone host family —
  an explicit --host can steer detection onto this reader while the cwd
  clone is a GitHub mirror (the dual-remote migration setup), which once
  emitted {platform:'aone', host:'github.com'} and queried a1 with the
  mirror's coordinates; same predicate fetchDiff's origin guard applies
- isPlainBranchName (both twins) rejects refs/-prefixed names: legal
  branch names (check-ref-format --branch) that resolve qualified refs
  the server controls as fetch/merge-base arguments (refs/remotes/origin/
  HEAD is the clone's default-branch symref — wrong base, misdescribing
  WARNING)
- the ref-dwim class is closed at the verified sites: fetch-pr's base
  probe fetches an EXPLICIT branch refspec (bare names dwim onto
  same-named tags — exit 0, tracking ref untouched, stale base passing
  the freshness guard it never refreshed), its fetchedSha/merge-base head
  reads are refs/heads-qualified (a planted same-name tag can no longer
  shadow the real head), and aone.fetchDiff's target fetch + merge-base +
  diff-range reads are qualified the same way

Round-12 is Critical-only per the convergence directive (the bot's own
ledger is past its round cap).
2026-08-18 09:19:52 +00:00
samuelhsin
506e8497d3
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-18 15:56:19 +08:00
易良
179c8f80fd
fix(memory): improve recall reliability and candidate coverage (#8716)
* fix(memory): improve recall delivery and multilingual fallback

* fix(memory): bound heuristic recall scoring

* test(memory): pin initial recall budget with fake timers

Rewrite the slow-recall test to assert with fake timers that the main
request is still held 1 ms inside the 100 ms initial budget and proceeds
without memory at expiry, so budget changes can no longer pass unnoticed.

* test(memory): pin recall budget and scoring contracts

Address review findings with mutation-verified pins:
- settle-early: bounded wait ends when recall settles, not at full budget
- Cron and ToolResult consume points stay zero-wait
- post-wait replacement guard refuses stale handles
- type boost flips the winner (tie-break no longer masks its removal)
- hiragana-only coverage for the CJK tokenizer
- design doc: RFC #7040 sets no numeric overhead target; fix attribution

* fix(memory): preserve recall field weighting

* fix(memory): recall relevant topics beyond scan cap (#8803)

* fix(memory): bound recall candidates after full scan

* test(memory): pin bounded selector inputs

* fix(memory): preserve bounded recall candidates

* fix(memory): preserve lexical recall candidates

* fix(memory): prioritize lexical model candidates

* fix(core): preserve UTF-16 manifest boundaries

* fix(memory): address recall review feedback

* test(memory): measure recall rollout gate against the pre-change scorer

RFC #7040 gates the multilingual precision change on evidence that English
Recall@5 and no-result precision do not regress. Add a labeled 45-case
corpus and an evaluation harness that scores both the shipped deterministic
selector and a frozen copy of the pre-change scorer over it, so the gate is
reproducible rather than asserted.

* fix(memory): deliver a deterministic fast recall result on the initial turn

The initial-turn budget is 100 ms, but recall awaits the model selector,
which is a network side query with a 30 s ceiling. The budget therefore
expires on the common path and delivery falls through to the ToolResult
point — which a tool-free turn never reaches, so the result is discarded as
no_safe_delivery_point. That is the case memory matters most for.

Publish the deterministic candidates that selectModelCandidateDocuments
already computes, before blocking on the selector, and inject them when the
budget expires. The refined result still lands at ToolResult, with documents
the fast phase already delivered filtered out.

phase telemetry now carries both stages: phase is the delivery stage, strategy
is the selection method, and they are orthogonal.

* docs(memory): record the fast-path decision and phase/strategy split

* test(memory): report the mixed-language slice in the rollout gate

* docs(memory): align recall docs on the deterministic fast path

memory-system.md documented recall selection but never documented delivery, so
the delivery telemetry from #7393 was undocumented and the fast path had no
home in the canonical reference. Add a delivery section and the delivery event
table, and correct two docs that still described the single-path behaviour.

* fix(memory): use Array<T> for the fast-path test doc lists

@typescript-eslint/array-type forbids T[] for non-simple types.

* docs(memory): clarify recall delivery telemetry

* fix(memory): report already-delivered recall count

* docs(memory): align recall delivery claims

* fix(memory): rank ties by recency and record fast-delivered discards

Three review follow-ups on the recall reliability change.

Tie-break: `selectRelevantAutoMemoryDocuments` broke score ties with
`type.localeCompare`, which orders feedback < project < reference < user.
That was tolerable while the result was five documents wide; the fast path
takes only MAX_FAST_RECALL_DOCS = 2, so a tied user-typed document was
dropped every time — the exact memory a tool-free turn exists to surface.
Ties now fall to recency, then to input order, which keeps the
project-before-user precedence the concatenation already establishes.

Corpus: the case labeled `semantic-no-lexical` had no relevant documents,
so it was a no-result case wearing the wrong label and nothing measured the
cost of "no lexical match, no score". Relabel it and add three genuine
answerable-but-lexically-disjoint cases. Both scorers return nothing for
them, so the slice sits outside the quality floor and is asserted
separately: the fast path closes the timing gap, not the matching gap.
Tool-free delivery is 92.3%, not 100%, and the residual is that slice.

Telemetry: a tool-free turn logs its terminal event from the discard path,
which did not apply the fast-phase exclusion. A turn whose every selected
document had already been fast-delivered was recorded as
`no_safe_delivery_point`, inflating the "memory never reached the model"
bucket with turns that got it. Apply the same rule the ToolResult consume
point uses; a partial overlap still reports the cancellation reason.

* docs(memory): state the candidate-cap trade and the per-turn document count

Two review follow-ups, documentation only. No behaviour change.

"Removes the 200-document cap" oversold the candidate change. What it does
is swap a per-scope, query-blind recency truncation for a global,
query-aware one, and the effect is not a uniform widening: at or under 200
documents nothing was excluded by count under either design, but the new
25,000-byte manifest budget is a ceiling the old path lacked; between 200
and 400 with neither scope over 200 the old path sent every document and
the new one sends at most 200, so fewer reach the model; only a scope over
200 is the case the change is actually for. Record all three, plus the fact
that the manifest budget packs rather than prefixes.

MAX_RELEVANT_DOCS = 5 bounds one prompt, not one turn. A fast delivery of
two plus a refined delivery of five disjoint documents puts seven in front
of the model; dedupe removes repeats, not the sum. This follows from
dropping combined fast/refined budget accounting, which was a deliberate
choice, but the number was never written down next to the constant that
reads like a hard cap.

* fix(memory): end the initial recall wait on the fast result, widen tokenization

The 100 ms initial budget was a fixed cost, and the evidence for it measured
the wrong thing. Deterministic *scoring* is microseconds, but the fast result
is only published once recall has enumerated, read, and parsed the memory
tree — and this branch removed the 200-document cap for recall, so that scan
grows with the tree. recall-scan-latency.test.ts adds that measurement
against a real temporary tree: ~29 ms at 200 topics, ~70 ms at 500, ~130 ms
at 1000.

So for any tree small enough to scan in time — the ordinary case — the fast
result was in hand tens of milliseconds before the budget expired, and the
rest of the budget was spent waiting on a model selector this design already
assumes will miss it. The wait now ends on whichever comes first: recall
settling, the fast result being published, cancellation, or the ceiling. The
preference order is unchanged, because the code after the wait still prefers
a settled recall. Past roughly a thousand topics the scan alone exceeds the
ceiling and the turn pays the full budget for nothing; that is recorded as a
known limitation rather than fixed, since the fix is a persistent catalog.

Tokenization kept only [a-z0-9]{3,} runs, so Cyrillic, Greek, Arabic, and
accented Latin produced no tokens at all and the deterministic path was
unconditionally silent for them. Keep whole runs of non-CJK letters, marks,
and digits instead. CJK is excluded per character rather than by alternation
order: \p{L} also matches Han, so a Latin-initial run would otherwise swallow
the CJK after it and turn abc漢字 into one token. Scripts without word
separators outside the CJK set still collapse to one run, which is recorded
rather than claimed as segmentation.

Two smaller follow-ups. The active-tool alias set is now derived once per
recall instead of once per scanned document, which mattered little under the
old 200-document cap and more without it. And the eval prints the Recall@5 a
query-blind random scorer would score on this corpus (20%), with a test
holding that floor at or below 25%, because a small corpus flatters every
design and the headline was unreadable without it.

* docs(memory): correct the initial-turn preference claim, pin it with a test

Local end-to-end verification on #8716 found the claim added in 01ef7d7d —
"the preference order is unchanged: whatever ends the wait, a settled recall
is still delivered in preference to the fast result" — to be false in the
case that matters. `onFastResult` is published before recall issues the
selector request at all, so the recall promise cannot be settled when the
wait ends on the fast result. Measured against a selector settling in 15 ms,
comfortably inside the ceiling, the initial turn still delivers the
deterministic pair and discards the model's picks.

The behaviour is right and stays: a model side query does not return inside a
100 ms ceiling in production, so arbitrating would spend the rest of the
budget on every turn to win a race that does not happen, and the selector's
judgement still lands at ToolResult with the fast documents excluded. What
was wrong was the description. State it directly instead — on the initial
turn, once the deterministic scorer matches, the fast result wins regardless
of selector latency — and pin it with a test that fails when the early exit
is removed, so it reads as a decision rather than an accident.

Two measurements corrected while here. The scan crossover is machine-
dependent, not a fixed topic count: the same three sizes measure 9/21/46 ms
on faster hardware against 29/70/130 ms on the machine the tables were
written from, so the ceiling is not reached there at all. And
MAX_MODEL_CANDIDATE_DOCS = 200 is rarely the binding constraint —
MAX_MODEL_MANIFEST_BYTES is, at roughly 90-150 documents once absolute paths
and timestamps are counted. Measured runs sent 94 and 96 manifest lines where
the document cap would have allowed 200, which also explains why the recency
reserve has to be interleaved rather than appended.

---------

Co-authored-by: yiliang114 <jinjing.zzj@gmail.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-18 07:50:26 +00:00
Shaojin Wen
30366b5faa
fix(review): gate the recovered incremental anchor on the model that certified it (#9184)
* fix(review): gate the recovered incremental anchor on the model that certified it

Incremental scoping is a same-model contract: "clean up to this commit"
is one model's verdict. The cache path has always enforced it through
lastModelId, but the anchor recovered from the posted review's ledger
marker shipped bare, so a round run under a different model would scope
sha..HEAD past code the current model never reviewed — permanently,
since each clean round re-anchors past the last.

The marker now carries the certifying model beside the anchor, riding
and falling with it: withheld on fail-closed and truncated rounds, and
dropped by the parser when the sha beside it did not survive. The
recovered-ledger context section names the model and instructs the gate
(absent counts as a mismatch — markers predating the field), and the
skill's incremental check requires a model match on both the cache path
and the marker-recovery path before scoping to the interdiff. The
findings work list still carries across models — every entry is
re-asserted against the code — only the anchor does not.

* fix(review): certify the ledger anchor with the runtime model identity

* fix(review): pin the posted marker's model wiring and tighten the anchor-gate spec

* fix(review): shed the anchor pair first and pin the round-3 findings

The marker's byte-cap loop dropped a finding before the anchor pair;
`dropped` then withheld the pair in the same render, so a capped clean
round lost a ruling it was owed. Shed the pair first — the work list
survives and recovery degrades to the full diff. Plus the round's pins:
attribution-off withholding of the runtime-injected model, the submit
fixture's production filename encoding, the skill's same-model gate
clauses, and the differing-SHA gate in the user docs.

* fix(review): scope the identity-channel claims and pin the branch-1 gate

The boundary comments and DESIGN.md claimed the runtime identity channel
delivers what the mechanism cannot: a model-authored command prefixes its
env, and the override reaches the child (measured in this repo's bash -c
spawn shape), so "the model the session ACTUALLY runs, not the id the
state JSON typed" overstated the guarantee. Scope every PR-owned claim to
what the wiring delivers — the runtime id supersedes the typed one, and
the channel stays forgeable, same posture as the cache path. Plus the
revert-guard's missing pin: branch 1's `If SHAs differ **and** model
matches` clause was unpinned, so a partial revert dropping only it left
every suite green (measured); the pin makes that revert fail and does not
misfire on the PR state.

* fix(review): shed the dead anchor tie-break and pin the reprieve clauses

* fix(review): stamp the round's model at capture, qualify it by provider

Two ways the same-model gate could certify a range under a model that
did not review it.

1. Deferred post. compose/submit read QWEN_CODE_MODEL at POST time,
   which tracks the session's CURRENT model — review under A, /model to
   B, "post comments" and the marker said B. The next round under B
   then scoped sha..HEAD past code B never saw. fetch-pr now stamps
   reviewModelId into its report when the diff is captured, and compose
   withholds the sha/model pair outright when that stamp disagrees with
   the runtime posting it: the round cannot name who reviewed the range,
   so it certifies nobody and the next round reviews in full. The
   findings still post.

2. One model id, two providers. A bare id is unique only inside one
   provider configuration; two of them exposing 'qwen3-coder-plus' would
   pass each other's gate. Config now publishes
   QWEN_CODE_MODEL_IDENTITY — <model>@<8-hex of authType+baseUrl> —
   beside the bare id, and the review flow prefers it. A runtime that
   publishes neither yields '', which reads as a mismatch, not as
   agreement.

The identity slot is process-global while the model is per-session, so
shellContextEnv hands it down only while it still describes the model
resolved for THIS session; a daemon side-session gets the bare id rather
than another session's qualification, since a confidently wrong identity
passes a gate the coarse one would have failed.

Every new test mutation-checked.

* docs(review): correct the absent-stamp and model-cap notes

The reviewModelId doc claimed compose reads an absent stamp as
"unknown"; it reads it as today's behaviour, and the reason is worth
stating — the report is written at the start of a round and read at its
end, so a missing stamp means an upgrade landed between the two, and a
runtime that publishes no model id empties the other side of the
comparison anyway.

The ledger cap's note predates the provider qualifier, which adds nine
characters to every id it bounds.

* style(review): prettier the reapplied round-model helper

* fix(review): rule the same-model gate in the CLI, key the identity per session

Four blockers from round 9, all in the identity plumbing this PR adds.

R9-1: the recovery path's gate could never fire. The marker's `model` is
the provider-qualified identity (`<model>@<digest>`), but SKILL.md told
the orchestrator to compare it against `{{model}}`, which
BundledSkillLoader substitutes with the BARE `config.getModel()` — two
identity spaces that are never equal, so every same-model continuation
round silently re-reviewed the full diff, which is the whole payoff this
PR exists for. Read loosely instead, a prefix match would have accepted
another provider's same-named model and re-opened the scope-skip the
digest closes.

The comparison now happens in the process holding both values:
`pr-context` renders the verdict — "the same-model contract HOLDS" or
"**Do NOT pass the reviewed-at sha as `--since`**", naming both
identities either way — and the skill obeys that sentence instead of
comparing strings. A section with no verdict is a mismatch. The cache
path keeps its bare-`{{model}}` gate: Step 8 writes `lastModelId` from
the same bare value, so that path is self-consistent.

R9-2: in daemon mode the identity leaked across sessions. The slot is
process-global and first-writer-wins, and withholding by OMITTING the
key is not withholding at all — every spawn site composes the child env
as `{...process.env, ...getShellContextEnvVars()}`, so the stale global
rode the spread and session B stamped its marker under A's identity.
Now registered per session beside the model (dropped together on
unregister) and written as `''` on a miss, the precedent the agent and
prompt ids in that file already set. The global slot stays the
single-session CLI's fallback, guarded so one that describes another
model is dropped rather than mis-qualifying this one.

R9-3 (×2): the two wiring tests never cleared QWEN_CODE_MODEL_IDENTITY,
which the boundary under test prefers — so an ambient value, which this
PR's own Config now publishes into every subprocess, overrode the model
they set. Running the suites inside a Qwen Code session is the
dogfooding path, so that was the normal case, not the exotic one.

Also folds the four inline `?? ` chains into lib/round-model.ts:
`roundModelIdFrom` and `certifierMatchesRound`, the latter pinning
whole-string equality and every unknown — absent certifier, unpublished
runtime, two blanks — as a mismatch.

Every new test mutation-checked.

* fix(review): make the blanked identity fall back, and drop the anchor pair whole

Round 10 filed no Criticals; these are the deferred items that were
defects rather than coverage gaps.

The R9-2 blanking silently disabled the bare-id fallback. `??` falls back
on ABSENT, not on empty — and the identity slot is deliberately written
as '' when a session has none to publish, because an omitted key is not
withheld (the spawn-site env spread leaks the parent's stale one). So a
blanked slot meant 'this round has no identity at all' rather than 'no
qualification, use the bare id': the round certified nobody and every
round after it re-reviewed the full diff. Both comments claimed the
opposite. Blanking must cost the qualification, never the identity.

`stripAnchor` dropped a foreign ledger's `sha` and left its `model`
behind — an identity certifying a range that is gone, which every reader
would have to know to ignore. They are written together, withheld
together by compose-review, and serialized only as a pair; they are
dropped as one now.

SKILL.md's recovery path is reached from a cache-path WITHHOLD too, not
only from an absent or refused anchor: a cache holding another model's
anchor stops the round at the cache, and the marker it never looks at
may hold one this model certified.

Five new tests, each mutation-checked: the blank-slot fallback, the
pair-drop, buildMarkdown's identity wiring, the per-session identity
registry (write and mid-session re-key), and `certifierMatchesRound`'s
engage case — every other case there is a refusal, so `return false`
survived them all.

* fix(review): rule the anchor verdict on the sha the side file actually holds

R11-3: the section's RULED-FOR-YOU verdict was rendered from the ledger
this run RECOVERED, while the sha Step 1 passes comes from the side
file — and `persistRecoveredLedger`'s never-lower-round guard
deliberately keeps a HIGHER-round file when the recovery walk comes back
short (a concurrent lane, a paginated fetch that returned less than it
should, a latest review deleted or edited).

In that state a HOLDS about the recovered sha is obeyed against a
different one, certified by whichever model ran THAT round — so the
round scopes past a range only that model reviewed, permanently, since
its own clean verdict re-anchors past it. Compose's drift gate cannot
catch it: the re-run re-stamps under the running model, so the stamp
agrees with the runtime and nothing looks wrong.

The verdict now rules on what the file HOLDS, read back off disk after
the persist decision rather than inferred from it — the guard's outcome
is exactly the thing a caller would get wrong by reasoning about it. A
divergence is a no-verdict state: both shas are named and the round
reviews the full range, because nothing available here can say who
reviewed the span between them. The findings still carry.

Two new tests, both mutation-checked: the renderer's divergence refusal
(and that agreement, and a file holding no anchor, still rule normally),
and `persistedAnchorSha` reading back what the guard actually kept —
the second is what fails when the read-back is stubbed out, which the
renderer test alone could not see.

* fix(review): move the last identity comparison out of prompt text

R12-1 and R12-2 are the sixth and seventh findings in one class — two
boundaries meaning different strings by the round's identity — so these
close the class rather than the two instances.

R12-1: the cache-path gate compared BARE ids on both sides. Step 8 writes
`lastModelId: "{{model}}"` and the gate compared it to `{{model}}`, both
the bare `config.getModel()`, so two provider configurations exposing one
model name passed each other's gate — the exact case the recovery path in
this PR rejects. Self-consistent is not sound; it was consistently wrong
across providers, and I deferred it last round as an asymmetry when it was
a hole.

The gate moves into `fetch-pr`, beside the one the anchor already goes
through: `--since-model` carries WHO certified the anchor, the skill
copies both fields verbatim, and `certifierMatchesRound` — the same
function the marker-recovery ruling uses — decides. A mismatch reports
`cross-model-anchor` and reviews the full range, refused before the
history is consulted at all.

That leaves ZERO identity comparisons in prompt text. Six rounds have each
closed one channel and the next round found another; the reason the class
kept regenerating is that a comparison written in prompt text cannot
share the CLI's notion of the string, and `{{model}}` is structurally the
wrong one — it interpolates the bare id where everything the CLI records
is provider-qualified. The SKILL guard now asserts the absence, not just
the presence: no `lastModelId equals`, no `model matches`/`model differs`.

R12-2: the drift gate disengaged whenever the post-time runtime channel
was blank, even with the plan's stamp proving the round STARTED under a
published identity — so `certifying` fell back to the model-written
`input.modelId`, the channel these docstrings retire. The recovery side
already rules an empty running identity a mismatch; the certifying side
does now too. An UNSTAMPED round still keeps its old behaviour, because
it cannot prove disagreement either.

Two new tests, both mutation-checked.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-18 07:43:40 +00:00
samuelhsin
0c058afb40
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-18 14:49:20 +08:00
BaboBen
9a64c0a963
feat(serve): add pollable daemon turn status (#9080)
* feat(serve): add pollable turn-status endpoints for daemon sessions

Add GET /session/:id/turns/current and GET /session/:id/turns/:promptId
so external callers can poll a turn's lifecycle state (queued / running /
completed / cancelled / error) and result instead of holding the SSE
stream for the whole turn lifetime.

- Live state comes from the bridge's pending prompt queue; settled
  outcomes from persisted turn_result transcript records, so results
  survive daemon restarts and the daemon keeps no per-turn memory
- Each prompt captures its own recording and settles exactly that one,
  so overlapping turns (DAEMON-003 deadline overlap) can never
  misattribute one turn's outcome to another promptId
- Enforces the same client authorization as POST /session/:id/prompt

Refs #8680

* test(serve): update telemetry route count

* fix(serve): prefer settled turn outcome over deadline error overlay

When the prompt-deadline path latches an error terminal in the overlay and the child later settles and persists a non-error turn_result for the same promptId, the poll surface previously kept the overlay error while enriching it with the successful resultText, and flipped to completed only after overlay eviction or restart. Merge via mergeTerminalWithPersisted at the two enrich call sites so the persisted outcome supersedes a bridge-synthesized error terminal once it exists; the exactly-once turn_error event publication and FIFO release are unchanged. The different-promptId endedAt tie-break is intentionally untouched.

* fix(serve): pin turn-start session identity for turn-result settle

R4-1: settle resolved the ChatRecordingService at settle time, so a startNewSession rotation mid-turn could land the turn_result record in the new session's transcript while the poll surface kept reading the old one. Capture the recorder at turn start and settle on that instance; pin the outgoing service's session identity at rotation so the late append keeps the pre-rotation sessionId.

R4-2: reject empty error.message/error.code in turn_result payloads, mirroring the existing empty-promptId rejection.

Adds rotation/pin regression tests plus the round-4 test suggestions (extractor fallback, startedAt, cancel/error race matrix, resultCode defaulting, removed-prompt projections).

* fix(serve): enforce the turn_result bounded contract on the write path

R3-3: cap promptId, stopReason, and originatorClientId at 256 chars in isTurnResultRecordPayload, closing the unbounded echo of corrupted-transcript values through GET /session/:id/turns/:promptId; recordTurnResult now validates payloads against the same contract before appending, so type-correct but invalid shapes (error state without error, error on non-error states) can no longer produce records invisible to the restart scan.

Also lands the four round-5 test assertions: merged-payload error-leak pin, multi-model-call settle count, successor attribution in the superseded-throws test, and the early session-mismatch guard pin.

* fix(serve): address round-6 review findings on daemon turn status

- Session: settle a successor-aborted turn as cancelled only when the
  thrown error is the abort itself; genuine failures after a NEW_PROMPT
  abort surface as error, matching the send-loop contract
- bridge: serve repeat polls of a settled promptId from the enriched
  overlay instead of re-scanning the child transcript, and give the
  turn-status read the transcript timeout instead of the 10s init default
- bridge: forward the channel display text unchanged; Session treats an
  empty display text as absent for the turn record ([image] fallback)
- Session: cap streamed-response accumulation for turns without a
  channel delivery at the turn-result bound
- docs: document the bounded non-monotonicity of poll terminals

* fix(serve): guard turn-status reads against rewind races and keep the trusted prompt projection

A successful rewind that completes while a getSessionTurnStatus child
transcript scan is in flight could let the pre-rewind record be cached
into the freshly cleared overlay and served forever. Track a per-session
rewind generation captured before the scan and discard the scanned
outcome when it moved.

enrichTerminalTurnStatus and the deadline-supersede merge returned the
child-recorded promptText ahead of the bridge's trusted display
projection, leaking hidden channel context on the poll surface. Make
promptText/promptTextTruncated backfill-only and keep the terminal's
projection in the supersede path. Make the pinning test adversarial and
correct a false comment about the child's ''-as-absent fallback.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qqqys <qys177@gmail.com>
2026-08-18 06:35:17 +00:00
ytahdn
721e435f7e
feat(web-shell): support upload directory and hard-disable drag-in when fileUploadEnabled=false (#9382)
fileUploadEnabled={false} previously still admitted file drags onto the
inline image/text lane, producing attachment tags with no upload. It now
disables file drag-and-drop entirely — no drag highlight, no drop ingestion,
no upload — while clipboard paste stays enabled. The composer core gates its
drag/drop handlers behind a new fileDragEnabled option; ChatEditor cancels
the drop outright.

A new fileUploadDirectory prop (relative to the workspace root) sets the
drag-upload target directory; the root remains the default. The daemon upload
route now materializes a missing target directory (recursively, depth-capped)
via a new WorkspaceFileSystem.mkdir, so a configured drop folder needs no
manual setup. mkdir follows the existing write-path safeguards: trust gate,
generation guard, path lock, audit, and symlink-swap re-checks per created
component and its parent.

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-18 05:04:52 +00:00
samuelhsin
d4e6dce207
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-18 11:53:48 +08:00
易良
99a3a08e17
feat(daemon): make serve new-file mode configurable (QWEN_SERVE_NEW_FILE_MODE) (#9364)
* feat(daemon): make serve new-file mode configurable (QWEN_SERVE_NEW_FILE_MODE)

qwen serve's atomic text writers created every NEW file at 0600
unconditionally, ignoring the daemon process umask with no way to opt
out (issue reporter runs the daemon under a systemd UMask=0002 drop-in
and every agent-created file diverged from the group-readable repo
convention).

Add a NewFileModePolicy ('owner' = 0600 default, 'system' = standard
0o666 & ~umask) on createWorkspaceFileSystemFactory, threaded through
writeTextAtomic / writeTextOverwrite / edit / editAtomic and the
same-host external tool-write route. resolveBridgeFsFactory reads
QWEN_SERVE_NEW_FILE_MODE ('owner' | '0600' | 'system',
case-insensitive); unrecognized values warn on stderr and keep the
fail-closed 0600 default.

Existing-file mode preservation is unchanged, binary uploads stay
0600, and the default behavior is bit-for-bit unchanged.

Closes #9250

* fix(serve): register the new-file-mode env access and harden the knob's docs and wiring

- process-env guard: register the whole-object process.env access in
  fs-factory.ts (the parseNewFileModePolicy default parameter) so the
  serve process.env guard suite passes — the PR-caused CI failure.
- resolveNewFileModeBits: read the umask lazily only when the 'system'
  policy consumes it; the default 'owner' path no longer issues two
  umask(2) syscalls per write.
- docs: state that the literal `0600` is an alias for `owner` (no other
  octal modes) in both tables; correct the binary-upload route to
  POST /file/upload; replace the phantom per-write mode-override clause
  with the factual statement that agents cannot pass one.
- test: pin the resolveBridgeFsFactory env seam — with newFileMode
  uninjected, the policy must come from process.env.QWEN_SERVE_NEW_FILE_MODE
  (regression-mutates to a hard-coded default are now caught).

* fix(serve): keep QWEN_SERVE_NEW_FILE_MODE out of project .env files

R2-1: the daemon boot path loads the primary workspace .env into
process.env before any fs factory is built, and the new-file-mode key was
not in PROJECT_ENV_HARDCODED_EXCLUSIONS — a project-controlled file could
flip the documented fail-closed 0600 posture to umask-derived modes
daemon-wide with no warning (system is a valid value), widening the
visibility of agent-created files on a multi-user host. Register it as a
process-scoped operator knob like the other daemon posture keys, with a
security test pinning the exclusion.

* test(serve): pin the fail-closed 0600 default through the resolveBridgeFsFactory seam

The env-wiring test only covered the 'system' half of the seam; the
unset-env default (owner -> 0600) had no coverage through the same
production path — a regression making the unset default resolve to
'system' would flip every agent-created new file to umask-derived
modes with no test failing (mutant verified surviving all 13 prior
tests; this mirror test fails it with 0o664 vs 0o600).

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-18 03:42:35 +00:00
ytahdn
789a6691ed
revert(web-shell): restore pre-#8098 composer animations at 50% opacity (#9349)
* Revert "fix(web-shell): stabilize mobile composer after resume (#8263)"

This reverts commit d213c8519a.

# Conflicts:
#	packages/web-shell/client/e2e/web-shell.composer.mobile.spec.ts

* Revert "feat(web-shell): enhance composer and empty-session animations (#8098)"

This reverts commit e379ea4e53.

* fix(web-shell): drop #8601 mobile-composer overrides superseded by the animation revert

The #8098 animation revert removes the position: relative / z-index: 1 on
.appChatEmpty .chatViewWrap, so #8601's position: static override (a fix for
the containing-block shift that #8098 introduced) is a no-op and its comment
describes a dot field that no longer exists. Sync the e2e layout assertions
to the restored pre-#8098 values (z-index auto, static wrap at 761px) while
keeping the footer-anchoring contract they verify.

* style(web-shell): cap the dac composer glow at 50% opacity

Design feedback: the restored composer glow reads too bright. Halve the
opacity ceiling for the aura grid, the halo and the pulse glow while keeping
the dac-aura-cols dance animation and the focus/pulse dynamics intact.

* fix(web-shell): keep the custom-footer wrap positioned for the bottom panels

Dropping the #8601 override removed the positioned containing block for the
bottom panels (status panel) in the custom-footer mobile welcome variant:
that variant renders the composer footer display: contents, so its wrap is
the only positioned ancestor the absolutely-positioned bottom panels can
anchor to. Restore position: relative on just that variant instead of the
full-positioned wrap the deleted rule had excluded, keeping the plain
variants static so the footer keeps anchoring to the chat pane.

* fix(web-shell): address review feedback on the animation revert

- Restore disabled?: boolean on the ChatEditor test render props: two
  surviving call sites still pass disabled: true, and dropping the member
  leaves object-literal excess-property violations (latent only while test
  files stay out of tsc).
- Update the stale chatViewWithCustomFooter marker comment in App.tsx: the
  :not(...) exclusion it described was removed, and the class now completes
  the compound selector that keeps the custom-footer wrap positioned for the
  bottom panels.
- Document the once-only glow effect invariant and the legacy data-dac-glow
  marker, both restored verbatim from the pre-#8098 implementation.

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-18 03:22:29 +00:00
samuelhsin
ec0fba218a
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-18 00:59:43 +08:00
易良
04043e555d
feat: consolidate Local Control into one daemon-owned implementation (#9106)
* feat(cli): add daemon-owned Local Control service

Local Control is implemented twice today — once in the CLI, once as an
830-line Rust TCP proxy in the Tauri shell — with two divergent security
models. This adds the daemon-side service both can collapse onto.

The Rust proxy exists only because `qwen serve` fixes its bind address at
startup and cannot add a listener later; everything it does (Host/Origin
rewriting, CRLF rejection, connection caps) is compensation for that one
fact. `LocalControlService` attaches a second `http.Server` over the same
Express app at runtime, so there is no hop to rewrite.

Phases 1-3 of docs/plans/2026-08-13-local-control-consolidation.md:

- Listener identity tagged on the `http.Server`, resolved per request, so
  credentials scope to the listener a request arrived on.
- `CredentialStore` replaces `bearerAuth`'s single pre-hashed token. The
  runtime token is rejected on the LAN listener and the pairing token on
  loopback — the invariant the Rust proxy enforced by rejecting requests
  carrying the runtime token. Fixes the CLI path handing the LAN the
  full-strength daemon token with no revocation short of restart.
- `hostAllowlist` now gates the LAN listener against its advertised
  authority. Previously it opted out entirely off loopback, leaving the
  CLI path with no DNS-rebinding defense.
- `MutableOriginAllowlist` lets the LAN origin be added and removed at
  runtime; the middleware is still installed once. Empty-allowlist
  behavior is identical to the `denyBrowserOriginCors` wall it replaces.
- ACP WS upgrade tracks a set of servers instead of one, and scopes the
  subprotocol credential the same way as the REST gate.
- LAN selection advertises private/link-local IPv4 only, and surfaces
  ambiguity to the caller instead of failing (Rust) or emitting a QR per
  interface (CLI).

Refs #9075

* feat(cli): wire Local Control into the daemon boot sequence

Constructs the service in `createServeApp`, where the credential store and
the CORS allowlist it mutates already live, and publishes it on
`app.locals` alongside `acpHandle` — the channel `runQwenServe` already
uses to reach into the built app for lifecycle work.

- `bearerAuth` now takes the listener-scoped `CredentialStore`, and the
  ACP WS mount takes the same one so the `qwen-bearer.*` subprotocol
  cannot sidestep the scoping the REST gate enforces.
- The CORS middleware is installed unconditionally over a
  `MutableOriginAllowlist`. With no `--allow-origin` this returns the same
  403 envelope as the `denyBrowserOriginCors` wall it replaces, so the
  default posture is unchanged.
- Daemon teardown disables Local Control before disposing the ACP handle,
  since detaching the LAN listener's upgrade registration goes through it.
  Token revocation and origin removal are synchronous, so they complete
  even though the enclosing dispose scope cannot await the socket close.
- The LAN listener honors `--tls-cert` / `--tls-key`, reading them at
  enable time so a renewed certificate is picked up. Serving plaintext off
  a daemon deliberately put behind TLS would downgrade the more exposed of
  the two surfaces; `status.encrypted` reports which it is.

Refs #9075

* feat(cli): repoint --local-control at the daemon service

The flag stops being a second implementation and becomes a caller.

Previously `--local-control` commandeered the daemon: it bound to
`0.0.0.0`, generated a token that WAS the daemon token, and rewrote the
origin allowlist — which is why it conflicted with `--token`,
`--hostname`, `--allow-origin`, and an ephemeral port. The daemon now owns
a separate LAN listener with a separate revocable credential, so none of
those are in tension. A daemon can serve authenticated loopback and run a
Local Control session at the same time, and `--no-web` is the only
remaining conflict.

- `localControlUrls` is deleted. Its "every non-internal IPv4" policy is
  the bug the service's private/link-local selection replaces; it would
  put a VPN or public address in a QR code.
- Ambiguous multi-network hosts get `--local-control-address <ip>` instead
  of a QR per interface.
- Sleep inhibition moves into the service, so it is held while the LAN
  listener is up and released when it goes down rather than for the
  lifetime of the process.
- The pairing line now reports actual sleep-inhibition and encryption
  state instead of asserting the common case.
- `RunHandle.getLocalControl()` reaches the service; a getter because the
  runtime app is mounted after the listener is up.

Refs #9075

* fix(cli): harden daemon-owned Local Control

* fix(cli): flush Local Control disable response

* feat(desktop): move Local Control into Settings

* fix(local-control): close listener lifecycle gaps

* fix(cli): resolve local control review comments

* fix(local-control): align route lifecycle

* fix(serve): close local control review gaps

* fix(serve): close Local Control QR and bridge-filter review blockers

QR rendering in the Local Control routes is now best-effort: an
over-capacity pairing URL (the target deep-link is caller-influenced)
no longer turns enable/status into a 500 while the LAN listener stays
live, which wedged the Web Shell card with no disable path. The
interface denylist also stops rejecting physical LAN bridges (br0,
Windows "Network Bridge") and only filters the virtual bridge shapes
(Docker br-<hex>, macOS bridge<N>), matching the deleted Rust filter's
per-platform behavior. Adds regression tests for both.

* fix(serve): close round-5 Local Control review findings

- Card: reconcile the selected LAN address on every status update, so a
  stale selection cannot survive a network change when only one candidate
  remains (the selector is hidden in that case and gave no affordance).
- Interface filter: fold the hex run into the Docker bridge token
  (br-[0-9a-f]+) so bridge IDs starting with a letter stop escaping the
  shared boundary check.
- LAN listener: drop the whole-request timeout budget; Node never resets
  it on body chunks, so it 408'd phones trickling large uploads through
  the shared Express app. Header and keep-alive timeouts stay.
- Copy: Ctrl+C ends the whole daemon, not just Local Control (design
  doc, terminal banner, --local-control description).
- Accessibility: aria-live on the card, role=alert on its error line.
- Tests: QR happy path, listen-error handler cleanup, strict
  error-handler count after enable, letter-starting Docker bridge.

* fix(web-shell): preserve local control base paths

* fix(local-control): close round-7 review findings

- card: keep the 409 candidate list on the error path — requestLocalControl
  attaches the parsed payload to the thrown error and toggle reconciles
  status/selection from it, so a stale address after a DHCP change recovers
  without a page remount (R7-3)
- lan-interfaces: match `vpn` as a substring and add a `wintun` token,
  closing the OpenVPN Wintun escape (boundary semantics let `vpn` sit
  inside "openvpn" unmatched) plus mid-word names like vpnkit; regression
  test covers the adapter family (R7-1 demonstrated entrance; structural
  per-platform classification stays a follow-up)
- drop the orphaned strictPort ServeOptions field, the EADDRINUSE-bump
  condition reading it, and its test — no production entry point sets it
  anymore (R7-5)
- docs: refresh 12-auth-security.md / 02-serve-runtime.md for the new
  middleware topology — unconditional allowOriginCors over the mutable
  allowlist on the runtime app, deny wall only in the bootstrap app,
  listener-scoped pairing credential on the LAN listener, and the new
  mutation-gate row (R7-2)
- remove the dead selectLanAddress barrel re-export

* fix(local-control): enforce the loopback-bind precondition on runtime enable

The LAN listener binds the primary listener's port on the selected LAN
address, so a wildcard or LAN primary bind already owns it — the
`--local-control` CLI flag refuses that configuration at boot, but the
runtime enable route (driven by the Web Shell Settings card) skipped the
check and surfaced a 500 `listen EADDRINUSE` with no remediation,
silently unusable for the whole class of non-loopback deployments.

Return 409 `local_control_non_loopback_bind` with the actionable
restart hint instead; loopback binds (127.0.0.1/localhost/::1) stay
enabled via the shared `isLoopbackBind` helper.

* fix(local-control): close round-8 demonstrated escapes + doc/test gaps

R7-1 (demonstrated false negatives): Docker veth peer IDs are
veth<hex> and may be letter-led (vethd4a1b2c), which the bare token's
digit boundary let escape — the token takes the same shape as br-<hex>.
Corporate SSL-VPN adapters (Cisco AnyConnect, GlobalProtect, Pulse
Secure, FortiClient, Cloudflare WARP) carry no `vpn` substring, so
their vendor names are listed explicitly; a sole-candidate VPN address
is no longer silently auto-advertised in the QR. Regression tests cover
all six shapes. The vEthernet-external false positive and the class fix
(structural classification instead of name matching) remain under #9158.

Also: the settings card's status-fetch effect clears a stale error on
re-run and ignores superseded responses; the detach test now connects a
primary-listener client and asserts it survives detachServer (the
per-server filter previously survived a mutation probe); the flags table
gains the --local-control-address row; the design doc states that
--allow-origin origins stay admitted alongside the LAN origin; the three
Host-gate doc surfaces note that the LAN listener always enforces its
advertised-authority Host check.

* fix(local-control): bound slow-body slots + close round-7 adapter escapes (#9106)

- service: replace requestTimeout=0 with a bounded 30-minute whole-request
  budget; an unlimited budget let an unauthenticated LAN client trickle
  bodies and hold every pre-auth connection slot open indefinitely
  (headersTimeout covers only headers, keepAliveTimeout only idle sockets)
- lan-interfaces: add interim vendor tokens for post-rename SSL-VPN
  successors (ivanti, cisco secure, citrix, sonicwall); Ivanti Connect
  Secure (Pulse Secure renamed) escaped the enumerated list and was
  auto-advertised in the QR. Class fix stays tracked in #9158
- auth: document the MutationGateOptions caveat that on a no-token daemon
  the Local Control pairing credential admits loopback callers to the
  strict surface (round-7 design decision still open)

* fix(local-control): stop serving the pairing secret to unauthenticated callers (#9106)

Probe-verified hole: on a tokenless daemon any local process could POST
/workspace/local-control/enable (or GET the unguarded status route) and
read status.url — the pairing token in the fragment — then present it on
the LAN listener, where the strictDenier passthrough admitted it to the
whole strict mutation surface (file writes, memory CRUD, git push/pull,
extension/MCP control) without the operator ever scanning anything.

Close the acquisition step:
- GET status / POST enable / POST disable now return url + qrText only to
  requests bearerAuth actually authenticated (requestWasAuthenticated);
  unauthenticated callers get the status with the secret stripped and
  urlRedacted: true while active
- on an unauthenticated enable the pairing URL is printed to the daemon's
  own terminal instead — the one channel a local attacker cannot read over
  HTTP
- web-shell Settings card renders a terminal hint when urlRedacted (en/zh)
- MutationGateOptions caveat rewritten to the resolved state

Authenticated callers (daemon token) are unchanged. Route tests: redaction
for unauthenticated GET/enable, full payload for authenticated callers,
terminal print on enable; suites 42/42, eslint/prettier clean, Codex
security review CLEAN.

* fix(local-control): close round-9/10/11 review findings (#9106)

- write the pairing URL with writeStdoutLineSafe so a dead/full stdout
  cannot wedge enable into a false 500
- reject an empty --local-control-address instead of silently dropping it
- pin --token/--allow-origin composition through to runQwenServe
- drop stale serve.ts file:line references in credentials/lan-interfaces
- correct the CORS caveat and the design doc's flag/origin claims

* test(serve): drop stale strict-port assertion

* fix(local-control): close round-12 review findings (#9106)

- give the composition test a full enable payload and a handle.close so
  the detached handler cannot leak a real process.exit(1)
- wrap the QR dynamic import + setErrorLevel in withUiData's fault
  isolation so a broken qrcode-terminal degrades to the raw URL instead
  of 500ing every status/enable
- fix the ZH urlRedacted copy (it prints a URL, not a QR)
- finish the denyBrowserOriginCors -> allowOriginCors doc sweep in
  01-architecture.md and 18-error-taxonomy.md

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-17 16:44:48 +00:00
samuelhsin
c2d17bf4fe
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-18 00:08:16 +08:00
jinye
f16975ee56
feat(serve): Add workspace session live-state endpoint and catalog version (#9261)
* docs(serve): Design workspace session live-state protocol

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(serve): Refine workspace session live-state design

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(serve): Add workspace session live-state endpoint and catalog version

Add GET /workspaces/:workspace/sessions/live-state: a memory-only
volatile snapshot (clientCount, hasActivePrompt, waiting flags) plus an
in-memory catalog version (generation+revision equality token), so
clients stop polling the persisted catalog for volatile state.

The bridge owns the clock: registration/removal marks flow through the
emitSessionLifecycle choke point; rename, automatic title, worktree,
and persisted branch commits mark at exact points; serve-layer REST/ACP
mutations share an invalidate-then-mark helper with exact no-op
semantics (deleted:false group deletes, removeSession:false cleanups,
no-op renames). The route exposes a new version only after
invalidating both persisted catalog scopes, enabling the client
live-A -> full catalog -> live-B reconciliation handshake.

Wire-additive: new unconditional capability
workspace_session_live_state, TypeScript SDK types and
DaemonClient/WorkspaceDaemonClient methods (native REST, no per-poll
capability preflight), telemetry label, and protocol/capability/SDK
docs. Required clock methods on AcpSessionBridge are a source-level
contract change for external structural implementations; in-repo fakes
updated.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(serve): Cover workspace_session_live_state in the serve integration baseline

The capabilities envelope E2E asserts the exact advertised feature list;
the new unconditional live-state capability must appear after the
archived-export tag, matching registry declaration order.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(serve): Close the handshake-vs-single-request argument gap

Separate the two claims the earlier paragraph conflated: the catalog
path cost is irrelevant to carrying a version (it runs anyway on a full
reload), but stamp placement decides consistency. Stamp-after-scan can
silently accept a bundle missing a mid-scan mutation; stamp-first is
safe and self-heals within one poll cycle, which a client may
legitimately choose. The A/B handshake buys provable consistency for
one extra cheap live-state read; the server supports both and the
Web Shell PR picks per product tolerance.

* fix(serve): Mark catalog version on persisted session renames

The metadata route's SessionNotFoundError fallback renamed persisted
sessions without advancing the catalog revision, so version-watching
clients kept the stale display name. Mark after a successful persisted
rename (parity with the live path, which marks on an actual change).

Also reconcile the design doc summary with its Implementation
Boundaries (the implementation ships in this PR, not a follow-up) and
spell out the child-recording persistence mechanism behind the
auto-title catalog mark.

* test(serve): Pin catalog-mark and live-state behaviors from the review round

- Assert markSessionCatalogChanged in the scheduled-task rollback
  (including the no-op-removal negative case), the sub-session and
  Live coordinator rollback paths, and the never-live orphan
  deletion; previously each mark could regress with suites green.
- Cover the live-state route's ?? false projection for both wait
  flags, and its first-exposure invalidation arm (revision
  unchanged, both organized scopes refilled).
- Cover the side-task generation-closed rollback arm (kill, remove,
  catalog mark).
- Compile the SDK live-state type fence via tsconfig.test-fence.json
  so shape assertions really pin the wire contract; the default
  tsconfig excludes test/.
- Align the design doc's cache-consistency goal with the cache
  mechanics (waiters joined before an invalidation may resolve, but
  cannot install).

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-17 15:01:50 +00:00
samuelhsin
1896b84c4d
Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-17 17:23:29 +08:00
jinye
2858538fb2
feat(core): Add privacy-safe tool-result boundary diagnostics (#9039)
* feat(core): add privacy-safe tool result diagnostics

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#9039)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-17 09:12:38 +00:00
易良
18c9763f46
feat(cli): add /advisor command for second-opinion conversation review (#7567)
* feat(cli): add /advisor command for second-opinion conversation review

Adds a manual /advisor slash command that asks a reviewer model for an
independent second opinion on the current conversation. The review runs
as a read-only forked side query sharing the main conversation context
(runForkedAgent cache path, NO_TOOLS), so the main session is never
mutated. A new advisorModel setting selects a dedicated reviewer model,
falling back to the main model when unset.

Refs #6542

* fix(cli): translate advisor command description

* fix(cli): address review feedback for /advisor command (#7567)

- Fix prompt to acknowledge transcript may be truncated (F1)
- Add empty history guard so fresh sessions get a clean error (F2)
- Add getModel() guard consistent with /btw
- Trim advisorModel to reject whitespace-only values
- Add cross-provider disclosure to advisorModel description
- Add ADVISOR_MAX_FOCUS_LENGTH constant
- Add i18n entries for advisor-specific strings (en/zh/zh-TW)
- Strengthen tests: section headings, abortSignal forwarding,
  empty history, whitespace model, no-override assertion

* fix(cli): refine /advisor abort handling, cache sharing, and docs (#7567)

* fix(cli): address /advisor review feedback — docs wording, i18n, test coverage (#7567)

* fix(cli): address /advisor review feedback — rendering, tools, guard (#7567)

Render the advisor review as a boxed markdown block (new MessageType.ADVISOR
+ AdvisorMessage) instead of a flat INFO line, so the four fixed sections
display as real headings. Always strip tools on the forked query (matching
/btw and the "no tools" prompt) rather than declaring them on the default
path. Tighten the busy guard to /recap's (isIdleRef + pendingItem) and return
an error message instead of addItem. Surface the resolved reviewer model in
the header (ForkedQueryResult.model) so a mistyped advisorModel that falls
back to the main model is visible. Move buildAdvisorPrompt and the input
limit to core advisor-utils for reuse, skip session recording like /btw, and
document the blocking-vs-/btw UX difference.

* test(cli): cover advisor inline code fences

* test(cli): cover advisor model setting schema

* fix(cli): address /advisor review feedback — docs accuracy, shared fence normalizer, test pins (#7567)

* test(cli): register advisor i18n keys as must-translate (#7567)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address /advisor review feedback — fence-aware normalizer, i18n keys, test pins (#7567)

* fix(cli): repair advisor test type cast that broke tsc build (#7567)

* fix(cli): address /advisor review feedback — parser-mirroring fence tracker, test pins (#7567)

* fix(cli): address round-4 /advisor review — fence tracker parser parity, raw-mode math gate (#7567)

* fix(cli): address round-5 /advisor review — normalizer corruption guards, wiring test pins (#7567)

* fix(cli): address advisor review gaps

* fix(cli): harden advisor reviews

* fix(cli): preserve ACP slash command cancellation

* fix(acp): preserve slash command lifecycle

* test(acp): verify advisor output without recording

* fix(cli): scope advisor ACP behavior

* fix(cli): require structured advisor output

* docs(acp): correct slash result support

* fix(cli): honor advisor context and JSON mode

* fix(cli): preserve sibling ACP slash command cancellation

* fix(cli): use shared recording skip set

* fix(cli): close advisor ACP telemetry gaps

* fix(cli): keep advisor JSON paths compatible

* fix(cli): close advisor telemetry compile gaps

* fix: resolve advisor transcript gate + response_format endpoint compat

R18-6: classify the /advisor recording gate by the resolved command's
kind instead of the raw input string, so user-defined commands shadowing
the name still record their prompt while the built-in /advisor stays out
of the transcript. handleSlashCommand now returns the resolved command's
name+kind alongside every result.

R19-1: gate buildResponseFormat on the official OpenAI endpoint, matching
the prompt-caching precedent; third-party OpenAI-compatible endpoints
(DeepSeek, older vLLM, validating gateways) reject the unknown
response_format field and this pipeline never sent it before.

* fix: key advisor recording skips on command identity + pin fallbacks

R18-10: the TUI recording-skip gate now matches the built-in /advisor
by kind+name instead of the bare name in SLASH_COMMANDS_SKIP_RECORDING,
so a user-defined command shadowing the name is recorded like any other
custom command. Regression test covers the FILE-kind shadow.

R18-3: pin the normalizeOpenAIStrictSchema -> json_object fallback with
the goalJudge-shaped partial-required schema and a typeless property.

R18-4: assert logConversationFinishedEvent fires on the fully-handled
non-advisor ACP slash-command path in the existing /btw test.

* fix(cli): record /clear user-turn before the session switch

R20-9: /clear (and its session-switching aliases) swaps in a fresh recorder inside its action, so its user-turn record must land before the action runs. Restore pre-resolution recording for every slash command except /advisor, which alone defers to after resolution so a user-defined command shadowing the built-in name keeps its record (R18-6).

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-17 08:54:18 +00:00
qqqys
2bbaafbf2f
feat(core): let a workflow agent pin a directory and outlive the default bounds (#8972)
* feat(core): let a workflow agent pin a directory and outlive the default bounds

Three gaps that together keep workflow subagents to short, in-place work.

**`agent({workingDir})`.** A script had no way to run an agent inside a
directory. `isolation: 'worktree'` is not a substitute: it CREATES a
worktree from the current tree and refuses to run when the parent tree is
dirty — the opposite of pinning an agent to a directory whose uncommitted
state is the point (a review worktree, a checkout a previous step
provisioned). `workingDir` is the same contract `AgentTool` already
exposes as `working_dir`: an existing, caller-owned worktree that the
harness neither creates nor removes.

Two details are easy to get wrong and both are covered:

- The fast path hands `config` to `AgentHeadless` untouched and cannot
  honour a rebind, so `workingDir` forces the override path. Left on the
  fast path it would be dropped in silence and the agent would run in the
  parent tree — the failure the option exists to prevent.
- `canonicalizeAgentOpts` now projects `workingDir`. The same prompt run
  against two worktrees is two different questions; without the
  projection a resume that changed only the directory would replay the
  previous tree's answers as this one's.

The validation moves to `agents/worktree-pin.ts`, shared with `AgentTool`
rather than duplicated: the path comes from a model either way, and
pinning replaces the child's `WorkspaceContext` wholesale, so it must
resolve inside the repository and be a registered linked worktree. The
caller passes the parameter name so errors say `workingDir` to a script
and `working_dir` to a tool call.

**Tunable per-subagent bounds.** `max_turns: 50` / `max_time_minutes: 10`
were hard-pinned at both dispatch sites with no override, while the three
other workflow bounds all have one. A build-and-test agent, or an
analysis of a 2 000-line file, exceeds them routinely — and under the
GOAL-terminal contract being cut off surfaces as a `null` element, an
agent that silently went missing rather than one that visibly failed.
Both are now env-tunable and clamped, and the doc comment states how
`stallMs`, `max_time_minutes` and the run wall clock differ, since
raising one without the others just moves which limit kills the run.

**Headless regression test.** A foreground `Workflow` call must complete
with no interactive session and no completion channel: `qwen --prompt`
has no TUI, no approval bridge and a closed stdin, so anything reaching
for interactivity inside the tool or runner would hang on a prompt nobody
can answer. The background half was already refused explicitly; this
pins the foreground half.

Part of #8769.

* fix(workflows): harden agent worktree pinning

* fix(workflows): anchor worktree-pin containment at the main working tree

Address the remaining review findings on the agent workingDir pin.

Containment anchored at `--show-toplevel`, which from inside a linked
worktree answers with the worktree's own root — spuriously refusing
registered sibling worktrees, the documented review-pipeline setup.
Resolve the repository's main working tree via the first entry of
`git worktree list --porcelain` (new GitWorktreeService helper) and
anchor there, keeping the toplevel answer as fallback.

Add dispatch-site wiring tests for the env-tunable subagent bounds at
both the fast and the override path: with a clean env the DEFAULT_*
constants and the resolvers are indistinguishable, so a revert mutation
at either call site kept every existing test green.

Cover the fs.realpath half of the containment guard with a real symlink
fixture (plain-string stubs made both realpath calls reject, so the
canonical-path logic never executed), and the sibling-anchor fix with a
unit case.

Correct the bounds doc comment: only QWEN_CODE_MAX_WORKFLOW_AGENTS and
the subagent bounds clamp to a ceiling; the stall and wall-clock env
overrides apply valid values verbatim.

* fix(workflows): refuse truncated main-tree anchors in worktree pinning (#8972)

A main working tree whose path contains a newline splits the porcelain
first entry of `git worktree list`, and the truncated prefix could
resolve inside a different repository — re-anchoring the pin's
containment and registration checks against that repo's worktree
registry. Detect the malformed first record (a path remainder where a
record attribute belongs) and fall back to `--show-toplevel`, whose
single-value answer keeps interior newlines intact.

Also pins down round-2 review findings: direct unit and real-git
coverage for `getMainWorktreePath()` (whose semantics were only
exercised through a stub), the symmetric journal-key HIT direction for
`workingDir` resumes, and the model-facing `workingDir` eligibility
description (the main checkout is not a valid pin target even though it
appears in `git worktree list`).

* fix(workflows): harden worktree pinning per round-3 review (#8972)

- Round-trip-validate the porcelain main-tree anchor (git-common-dir must
  agree) so attribute-shaped or trailing-newline truncations cannot aim
  the pin gate at a different repository's worktree registry
- Preserve legitimate path whitespace when parsing the anchor and the
  --show-toplevel fallback (terminator-only strip, untrimmed raw output)
- Thread one canonical realpath through both pin gates and the rebind so
  a re-pointed symlink cannot land the child where neither gate looked
- Canonicalise both containment sides or neither, so an absent target
  reaches the registration gate's accurate message instead of a
  manufactured outside-the-repository refusal
- Name the degraded anchor in the containment refusal when the main
  working tree could not be determined
- Throw on agent({workingDir, isolation}) at the orchestrator entrance
  (revived plain object — not evadable by the sandbox getter trick)
- Trim-based blank check for workingDir at both workflow entrances
- Document stallMs in the workflow schema and extend the tool-level
  capability enumeration to workingDir and stallMs

* fix(workflows): type worktree-pin test stub as nullable per service (#8972)

The round-3 degraded-anchor test passes null to the getMainWorktreePath
mock, but the vi.hoisted stub inferred Promise<string> from its default
implementation while GitWorktreeService.getMainWorktreePath() returns
Promise<string | null> — tsc --build failed on the clean rebuild of
packages/core. Annotate the stub with the real service signature.

* fix(workflows): harden stallMs gate and test portability per round-4 review (#8972)

- Reject non-numeric agent({stallMs}) loudly in the sandbox gate instead of
  silently dropping it to the default 60s watchdog, which contradicted the
  advertised "0 disables the watchdog".
- Compare worktree paths via path.resolve/path.normalize in the new tests so
  the windows-latest unit lane stops failing on separator differences.
- Correct the model-facing workingDir/stallMs descriptions to match the
  registry-only gate and the first-progress-event arming semantics.
- Restore the Agent tool's historical "a sub-agent" refusal wording.

* test(webui): deflake same-session refresh transcript assertion (#8972)

The failing CI annotation named keeps-the-attachment-live (load variant):
the live agent_message_chunk was asserted after a single-macrotask flush,
but under runner contention the batched setTimeout(0) dispatch can land one
tick after that window, so the chunk reads as missing even though it is
delivered. Replace the fixed-depth read with a bound-wait (vi.waitFor) for
the exact same blocks, preserving the assertion. Verified with 10 repeated
full-file runs under load (all failing before the fix, all passing after).

* docs: note the workflow workingDir opt is stricter than working_dir (#8972)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): sanitize workingDir echo in workflow refusal errors (#8972)

The workingDir refusal throw interpolated the model-authored path
through JSON.stringify only, which escapes C0 but leaves DEL and the
C1 range (incl. NEL U+0085) raw in the error message. Route the echo
through sanitizeForErrorMessage like the sibling agentType path, and
add a regression test that fails without the fix.

Also address R7 review suggestions: name the
QWEN_CODE_WORKFLOW_STALL_SECONDS override in the stallMs description
to match every other env knob there, anchor the new workingDir/stallMs
prose in the description regression test, and make the bounded
runConfig test hermetic against the two new env knobs.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): name the subagent turn/time env caps in the workflow tool description (#8972)

* fix(core): preserve trailing CR in git worktree path answers (#8972)

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-17 08:34:03 +00:00
ytahdn
c0791f0450
feat: support session media references end-to-end (#9310)
* feat: support session media references end-to-end

* fix(cli): catalog session media routes and update mid-turn replay meta test

The three new media routes (POST/GET/DELETE /session/:id/media[/:mediaId])
were registered but missing from legacySessionTelemetryRoutes, tripping the
route drift guard; add them as handler_resolved like their sibling routes.
The mid-turn history-replay expectation now carries the replay meta this PR
adds (source: mid_turn_message_injected, qwenDiscreteMessage: true).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(daemon): harden session media per review feedback

Addresses the Critical review findings on the session-media PR:

- Reject image/svg+xml uploads and serve stored media with
  Content-Disposition: attachment and X-Content-Type-Options: nosniff
  (same-origin XSS vector on the daemon/Web Shell origin).
- Keep the retained-media TTL sweep running when the session reaper is
  disabled (sessionReapIntervalMs <= 0) on the default 60s cadence.
- Record only the inline bytes the media references actually cover:
  the gate now counts image blocks only (references are image-only),
  and the strip keeps unrelated inline parts (e.g. @-mentioned files).
- Show '[User message with attachments]' on TUI resume for image-only
  mid-turn messages recorded with an empty displayText.
- Exempt mid-turn injected echoes from the Web Shell status-noise and
  plan-JSON filters.
- Degrade refresh-rebuilt queue rows to summary-only when media
  hydration failed, so editing cannot silently discard attachments.
- Retry cross-session media removal without the clientId when the
  daemon rejects the stale persisted id (invalid_client_id).
- Register session_media in the integration capabilities baseline.

Each fix carries a regression test that fails on the pre-fix code.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(daemon): bound media content and fix resume/echo edge cases

Addresses the remaining Critical review findings on the session-media PR:

- Cap media content blocks at 256 on the mid-turn and prompt routes and
  resolve each distinct mediaId once per resolveContent call — an
  unbounded array of duplicate references amplified one small request
  into gigabytes of heap at dispatch.
- Record a '[User message with attachments]' placeholder for
  inline-media-only mid-turn messages with no references, keeping ''
  only for the reference shape that replay projects.
- Restore the same placeholder for image-only ordinary prompts on TUI
  resume instead of dropping the message from the restored history.
- Treat a mid-turn injected echo as renderable when its items carry a
  non-empty text block, so the degraded-media echo (messages: [''] plus
  the placeholder text block) is not discarded as malformed.
- Release session media in killSession's force-kill and closing-session
  fallback branches instead of degrading to the crash-path detach
  retention.
- Remove the unreachable duplicate return in DaemonSessionClient.load().

Each fix carries a regression test that fails on the pre-fix code.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(daemon): resolve round-4 session media findings

Addresses the round-4 Critical review findings on the session-media PR:

- Move the mid-turn media-reference validation below the idempotent
  retry-ack rings so a same-id retry whose media was already removed
  (delete racing an in-flight POST, or a refresh re-enqueueing from the
  snapshot) settles idempotently instead of failing with
  session_media_gone (410).
- Keep image/* (unknown mime type) prompt images inline instead of
  uploading them: the media route matches concrete image types only, so
  the upload POST 400s and the whole submission hard-failed, regressing
  pre-upload behavior for untyped images.
- Project the degraded-media drain echo's placeholder text block when
  the echo text is empty, so the Web Shell shows the unavailability
  notice instead of rendering an empty bubble.

Each fix carries a regression test that fails on the pre-fix code.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(daemon): resolve round-5 session-media critical findings (#9127)

- Stop deleting a session-media blob when one queued prompt / mid-turn
  message referencing it is removed: the store has no reference counting,
  so siblings, replay metadata, or other clients may still hold the same
  mediaId. Blobs now live until session close / TTL sweep or an explicit
  removeSessionMedia.
- Reject duplicate mediaId occurrences in one message at assertReferences
  (covers the prompt and mid-turn admission paths); the serializer expands
  every reference at dispatch, so repeats amplified one upload into an
  unbounded payload even though only one read is needed.
- Align the mid-turn display-text and reference-persistence gates: compute
  the same willPersistReferences condition before finalizing displayText, so
  a partially-referenced message records the attachments placeholder instead
  of an empty displayText with no references.
- Keep the webui mid_turn_message_injected sidechannel alive for degraded
  image-only echoes whose items carry only the placeholder text block,
  mirroring the SDK normalizer's hasRenderableItemContent.

* fix(daemon): resolve round-6 session-media critical findings (#9127)

* fix(daemon): resolve round-7 session-media critical findings (#9127)

* fix(daemon): resolve round-8 session-media critical findings (#9127)

* fix(daemon): correct session media recovery and queue isolation

* fix(daemon): remove media with deleted queue items

* fix(daemon): bound repeated media in queue drains

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-17 07:59:41 +00:00
云胧
22be314c69 Merge branch 'main' into codex/feat-9123-web-shell-skill-refresh 2026-08-17 15:53:38 +08:00
ytahdn
addde7c1d0
feat(web-shell): decouple composer from catch-up and rebuild SSE on disconnected submit (#9323)
Catch-up (SSE history replay) no longer disables the composer input or
swaps its placeholder to "加载中": replaying history does not conflict
with typing, and a stuck catch-up must never lock the input.

A prompt submitted while the SSE stream is down is no longer blocked with
a toast: admission rebuilds the stream immediately by aborting the
reconnect backoff and resuming via Last-Event-ID, keeping the session
handle intact.

Drops the now-unreachable 'loading' placeholder state from the public
WebShellComposerPlaceholderState type and syncs docs (README, design doc,
provider and entry type comments).

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-17 06:39:23 +00:00
ytahdn
9a5b07c0b1
feat(web-shell): improve sidebar session management (#9311)
* feat(web-shell): improve sidebar session management

* feat(web-shell): keep sessions accessible when sidebar is collapsed

* test(web-shell): cover hover session details

* test(web-shell): align workspace sidebar coverage

* fix(web-shell): keep collapsed session actions open

* fix(web-shell): layer collapsed session menus

* fix(web-shell): address sidebar session details review findings (#9122)

- sdk: route workspace session metadata PATCH through direct REST
- collapsed switcher: cancel stale hover-close timers on reopen, keep the
  surface open while the group picker or keyboard focus is inside it, and
  emit the missing close signal when a tracked menu unmounts open
- suppress the session menu's close focus restore when it started a rename
- migrate the primary workspace expansion preference across the
  provisional-to-resolved cwd key change
- honor a persisted workspace collapse over stale one-shot auto-expansion
- drop the inert archived-row tab stop
- align the constrained smoke test with the single-line details title

* fix(web-shell): resolve standing sidebar session details blockers (#9122)

- collapsed switcher: resolve pointer targets through composedPath and
  make the close timer's focus guard shadow-DOM aware so hover-open
  containment works in shadowDom portal mode
- reset search state when the sidebar collapses so the autofocused
  search input no longer mounts inside the hover popover and steals
  keyboard focus
- rename: propagate the daemon-resolved displayName (clamped to 256)
  instead of the locally typed string and cap both rename inputs at
  256 characters
- keep the session list scrollable clear of the fixed footer so rows
  stay hoverable, and close the details popover before each
  constrained re-hover in the smoke test
- projects section: write the expansion preference outside the state
  updater, never lock hideProjectHeader consumers behind a stored
  collapse, and reset the one-shot show-all per session source and
  primary workspace
- stop a double-click inside a mounted rename input from restarting
  the rename and discarding the typed text

* fix: address round-6 review findings in serve metadata and sidebar (#9122)

- serve: reject empty/whitespace displayName on the workspace metadata
  route so archived sessions never persist an empty custom_title record
- serve: advertise workspace_session_metadata in the integration
  capability baseline to match the registry and unit baselines
- sidebar: end the session scroll port above the fixed footer so rows
  can never park under it and block hover (drops stale clearances)
- sidebar: reset search state whenever the collapsed surface closes so
  a stale autofocused input cannot steal composer focus on hover-open
- sidebar: keep keyboard-opened collapsed switcher in keyboard
  semantics; a pointer graze no longer suppresses focus restoration
- sidebar: busy-guard the archived rename menu item, align the
  group-create icon with its siblings, and reset per-section show-all
  on session-source change to match the flat list

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: address round-7 review findings in serve metadata and sidebar (#9122)

* fix: address round-8 review findings in sidebar rename and switcher (#9122)

* fix: address round-9 review findings in sidebar rename and menus (#9122)

* fix: address round-10 review findings in sidebar actions and rename (#9122)

* fix(web-shell): remove unsafe rename unmount cleanup

* fix(web-shell): stabilize sidebar session mutations

* fix(web-shell): polish sidebar session interactions

* feat(web-shell): complete collapsed sidebar navigation

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-17 06:06:56 +00:00
云胧
7fb2d8a088 Merge branch 'feat/9024-skill-toggle-mutation-metadata' into codex/feat-9123-web-shell-skill-refresh
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 13:11:29 +08:00
samuelhsin
5abd367f47
feat(daemon): attach skill-toggle mutation metadata to settings_changed (#9051)
* feat(daemon): attach skill-toggle mutation metadata to settings_changed

Hosts can apply Skill toggles incrementally without a full task reload or suppressing skills.* events.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(review): fit skill-toggle mutation metadata in the SDK bundle budget

The new normalizer parser pushed the browser daemon bundle over the 186KB cap. Raise it to 187KB and pin the review gaps that were cheap to close.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(daemon): pin skill-toggle mutation event count and parser edges

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sdk): raise daemon browser bundle budget for skill-toggle metadata

The 190KB cap overflowed by 491 bytes after merging main, so the SDK build fails before tests run.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-17 05:01:41 +00:00
samuelhsin
c131b3f581
Merge branch 'main' into feat/9024-skill-toggle-mutation-metadata 2026-08-17 11:59:59 +08:00
qqqys
a1e046eb6c
feat(core): add a live-session registry and qwen sessions ps (#8969)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(core): add a live-session registry and `qwen sessions ps`

Records each interactive session at `~/.qwen/sessions/<pid>.json` while it
runs, so "which Qwen Code sessions are on this machine right now" is one
readdir instead of a walk over every project's transcript directory.

This is the discovery surface cross-session messaging needs
(QwenLM/qwen-code#8724), landed on its own because it is useful by itself
and changes nothing about how a session behaves.

Why not extend the existing runtime.json sidecar: it lives under
`<projectDir>/chats/<sessionId>.runtime.json` and is never unlinked, so
presence carries no liveness signal. The cost of asking it this question
is visible in `isSessionRuntimeActive` — ~150 lines of candidate-directory
guessing plus a recursive scan, and that only answers whether one *known*
session is alive. Enumerating every live session that way is that cost
times N. The two coexist: runtime.json stays the kimi-compatible "which
session is PID X serving" sidecar for external observers.

Staleness is decided by PID liveness plus a start identity of
`<boot_id>:<starttime>` read from /proc, so neither a recycled PID nor a
reboot can resurrect a dead session's record. `session-writer-lease.ts`
composes the same Linux identity and is deliberately left alone — its
token is a persisted format with takeover semantics. The new
`process-liveness` helpers replace the private copy in teamHelpers.

Registry hygiene worth calling out, each one a real failure mode rather
than defensive habit: the directory is chmod 0700 on every register
(mkdir's mode is umask-masked and does nothing for an existing
directory); records are 0600 and written `noFollow`, so a pre-planted
symlink cannot redirect a registration write; only `<digits>.json` is
ever considered a record, because a lenient prefix match would read
`2026-planning-notes.json` as PID 2026 and delete a file this code never
wrote; and a record that fails validation is skipped without being swept,
since we cannot reason about what we cannot parse.

`qwen sessions ps` prints the live sessions; `--json` emits JSON Lines.
Record fields come from other processes, so the table renders them
through `sanitizeTerminalText` — ANSI, control bytes, and bidi overrides
(CVE-2021-42572 class) all matter when DIRECTORY is the column a user
relies on to tell two sessions apart.

Registration happens after first paint: nothing on screen depends on it,
and it is an mkdir plus an fsync'd write. `/clear` and `/resume` patch the
record's session id, and a directory switch patches its cwd, but never its
name — that name is the handle a user just read out of `ps`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): harden the session registry per review feedback (#8969)

* fix(core): guard session registry identities per machine and boot (#8969)

* fix(core): harden session registry identity guards under boot-id and schema outages (#8969)

Boot-id unreadability now degrades the write paths (register/patch/
unregister) to accepting only tokenless records and never disables the
reader-side cross-machine guard, so a foreign machine's live record can
no longer be overwritten, merged into, unlinked or swept during an
outage. readRecord discriminates newer-schema records from torn files
and the write paths refuse them like foreign-identity records instead of
treating them as unowned. Registration on Linux retries the start token
once and refuses rather than writing an impersonable tokenless record.
The /cd refresh queues the sidecar write and the registry patch as
separate entries so a sidecar failure cannot skip the patch, and patch/
unregister reuse the record path captured at registration so a relative
QWEN_HOME resolving against a moved cwd keeps working. deriveSessionName
NFC-normalizes, keeps combining marks, and truncates by code point.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): keep the review-context manifest under the resolved-file bound (#8969)

The committed-manifest tripwire failed once main's coordinate skill
landed: the every-rule-co-matched relatedPaths expansion grew from 128
to 129 resolved files, one past the wire bound. Narrow the core-skills
rule's relatedPaths to the infrastructure files directly under
packages/core/src/skills/ — bundled skill content is self-contained,
arrives in the diff itself when it changes, and grows with every new
bundled skill, so leaving bundled/** in the glob would spend the
bound's headroom on each addition. All fail-closed bounds stay pinned.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): close session registry race and outage gaps per round-4 review (#8969)

Refuse registration when the Linux PID-namespace id stays unreadable
(the record would be unreclaimable litter poisoning its PID slot),
retry once like the start token. Treat non-ENOENT read failures as
intact-foreign ("read-error") in register/unregister instead of
unowned. Require a readable matching start token before patch merges.
Re-read a record before the sweep unlinks it so a registration winning
the window is not deleted. Tolerate ENOSYS/ENOTSUP on the registry-dir
chmod. Move registry patches off the sidecar write chain onto their
own never-awaited chain so a rejecting or hanging sidecar write can
neither skip a patch nor hang /cd on the HOME write.

* fix(core): serialize session registry lifecycle

* test(cli): update session registry mocks

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-08-17 03:53:12 +00:00
samuelhsin
3c46058a4a
Merge branch 'main' into feat/9024-skill-toggle-mutation-metadata 2026-08-17 10:17:30 +08:00