Commit graph

15 commits

Author SHA1 Message Date
Shaojin Wen
46bf492d05
feat(managed-agent): bound half-open Hosted event streams behind a keepalive capability (#14135) (#14512)
* feat(managed-agent): bound half-open Hosted event streams behind a keepalive capability (#14135)

The Harness advertises its events-route keepalive as event-keepalive/1,
and the Java client arms the idle watchdog only against a Harness that
advertises it, so a rolling upgrade never aborts the quiet streams of an
older Harness. The abort is now the typed HostedHarnessStreamIdleException:
the coordinator retries it through the existing re-attach path and never
settles or cancels the Turn on silence alone. The watchdog ticks every
tenth of the bound, so an abort lands 45 to 49.5 s after the last byte.

HostedEventStreamLivenessIT runs the probes on the packaged chain behind
a TCP relay that holds the events connection half-open or stalls it, and
the hosted-harness-mysql fork budget grows to 4793 s. A design note
records the numbers, the capability, the outcome and the other Hosted
streams.

Closes #14135

* test(managed-agent): frame relayed requests and see virtual-thread readers in the liveness gate

The event-stream relay classified a connection, rewrote its Host header
and matched keepalives within single reads, so a request or a keepalive
split across TCP segments escaped the stall and half-open injection or
the cadence count. It now buffers each request head, forwards the body
by its Content-Length, matches keepalives across reads, and records and
resets a request it cannot frame. HostedEventStreamRelayTest pins each
split.

The half-open probe checked reader release with Thread.getAllStackTraces(),
which leaves out the virtual threads Turn readers run on. It now reads a
JDK thread dump, and first requires that dump to show the parked reader.

* test(managed-agent): count each keepalive one relay read carries (#14135)

The relay recorded one stamp per read that held a keepalive, so two keepalives in one read counted once, and the split-keepalive case could report a size of 1 whenever the pieces happened to land together. It now counts every match in the read window; the carried tail is a byte shorter than a keepalive, so none counts twice. A new case sends two keepalives in one write.

* test(sdk-java): park the published-window probe mid-interval (#14135)

The watchdog's ticks run from the stream's creation, and the probe parked right after it. A 2000 ms bound is a whole number of half-budget 1000 ms ticks, so that park sat on a tick of the mutant too, and wherever the scheduler ran a few ms late (macOS) the mutant aborted at ~2000 ms and passed. Parking 300 ms in puts the park mid-interval for both ticks: the shipped 200 ms tick aborts at 2100 ms and a 1000 ms tick at 2700 ms, so the limit drops to 2400 ms.
2026-10-18 14:14:37 +00:00
Shaojin Wen
d565d7463d
fix(sdk-java): Close Hosted Harness review criticals from #12654 (#13314)
* fix(sdk-java): Close Hosted Harness review criticals from #12654

#12654 merged before its second review round was processed. This closes
the 11 Critical and 2 Minor findings, each re-verified against main and
pinned by a new HostedHarnessClientTest case whose mutation witness was
run (reverting the fix reddens the test):

- HarnessEventStream close() no longer parks behind a next() blocked in
  the socket read: the closed flag is atomic, the raw input is closed
  without any stream monitor, and HostedHarnessClient.close() completes;
  the blocked consumer fails with DaemonTransportException (R1-1).
- The hosted SSE transport gains the idle watchdog its stdio sibling
  has: Builder.sseIdleTimeout defaults to 45s, and a silent peer makes
  next() fail with "Hosted Harness SSE idle timeout" (R1-43).
- submitTurn releases the active-prompt slot on a definitive 409 and
  throws PromptAlreadyActiveException; retaining a marker keyed by the
  rejected promptId wedged the session client-side (R1-2). A definitive
  non-409 failure on a same-identity retry releases that slot too (F2).
- getStatus() snapshots the entry before the remote read and removes
  only that registration, so a stale hasActivePrompt:false cannot delete
  a different prompt admitted in flight (R1-4).
- Responses missing the X-Qwen-Harness-Boot-Id fence header on mutation
  paths are classified outcome-unknown (create/prompt/mutation/delete)
  instead of a bare DaemonProtocolException (R1-5).
- A local executor rejection (saturated or terminated pool) surfaces as
  DaemonTransportException instead of claiming the request may have
  reached the daemon (R1-45).
- closeSession(ref) and detachSession treat a definitive 404 as done and
  tear down the heartbeat timer (R1-3, R1-46).
- streamEvents falls back to the session ref's validated watermark when
  the builder omits the resume pair, so the epoch fence actually
  applies (R1-44).
- The shared SSE parser accepts the id-less session_snapshot frame, so
  StreamHarnessEvents.snapshot(true) works (R1-47).
- computePayloadDigest hashes a sorted-key canonical form, so the wire
  dedup key is reproducible across JVM processes (R1-6).
- HarnessRuntimeExecutionRecovery requires status for known outcomes at
  construction instead of NPE-ing in isContinuationReady() (F1).

Test: HostedHarnessClientTest 34/34 (15 new); full module 181 green with
9 env-gated skips on JDK 17 and clean test green on JDK 11; 14 mutation
witness arms all red. The remaining 49 Suggestion-level findings are
dispositioned in their review threads and tracked in #13313.

* fix: Close second-round review criticals on the Hosted Harness client

Addresses the CHANGES_REQUESTED round on #13314, which execution-proved
one real regression in the first commit plus five more critical defects
around it:

- R1-1 (the regression CI confirmed on the fault-gates lane): the digest
  was canonicalized but the wire `prompt` member was not, and the hosted
  route re-derives the digest from the received bytes — every addText
  submission drew `400 invalid_hosted_prompt`. toJson() now emits the
  canonical form, the digest reuses the JsonSupport.encode facade over
  that same value, and the test asserts the wire bytes and the digest
  are literally the same value.
- R1-3: the 45s idle watchdog was inherited from the stdio transport,
  whose server heartbeats; the hosted events route wrote nothing while
  no event was committed, so healthy silent streams (tool calls, the
  10-minute approval wait) were aborted. The hosted route now mirrors
  the primary route's keepalive comment every 15s, and
  Builder.sseIdleTimeout accepts Duration.ZERO for callers that own the
  deadline. A keepalive-only peer is covered by a new test (consumer
  parked in read for three idle budgets).
- R1-4: an idle abort while the consumer was between next() calls
  surfaced IllegalStateException (not the DaemonException family). The
  closed guard now distinguishes who closed the stream: idle abort ->
  DaemonTransportException("Hosted Harness SSE idle timeout"),
  caller close -> IllegalStateException, matching the javadoc.
- R1-5: the cause-chain scan certified any rejection-shaped IOException
  as "never dispatched", but delivery also runs on the caller executor,
  so a rejection after the server accepted the prompt dropped the
  active-prompt marker for an admitted turn. Only the bare
  RejectedExecutionException (submit-time) and the terminated-executor
  case now classify as saturated/known; everything mid-flight stays
  outcome-unknown. A deterministic delivery-phase test drives this via
  a package-private executor seam (park the worker, fill the queue,
  then answer 202).
- R1-6: the missing-boot-header wrap reclassified every unfenced
  response as outcome-unknown, including definitive 4xx the server
  emits itself before the fencing middleware (413, 429). All four
  sites now re-derive: ambiguous (408/5xx) keeps outcome-unknown,
  definitive unfenced statuses surface as DaemonHttpException with
  their status preserved.
- R1-7 (suggestion folded in, same hunk family): detachSession tears
  the local attachment down for every definitive answer, not just 204
  and 404 — a 403/400/410 no longer leaks a heartbeat timer.
- R1-13 (suggestion, adjacent): a caller queued on cursorLock when the
  stream closes re-checks inside the lock and gets the closed-stream
  signal instead of a transport error.

Also: the snapshot frame comment now names the actual producer (the
bridge's withSnapshot on the primary events route — never the hosted
route); the StreamHarnessEvents builder javadoc documents the
lastEventId fallback precedence and that snapshot(true) is a
primary-transport option; the README retry section and both design-doc
languages now state the canonical digest as a cross-language wire
contract and record that changing the digest algorithm requires
quiescing or rewriting in-flight turn rows (R1-2's deployment note);
the two 10ms heartbeat tests assert liveness instead of comparing
zeros; and an explicit-cursor-wins case plus a definitive
boot-headerless DELETE case close the two remaining unpinned arms.

Test: HostedHarnessClientTest 42/42 (full module 189 green, 9
env-gated skips) on JDK 17; clean test green on JDK 11 (2 additional
version-gated skips: JDK 11 parks such sends instead of surfacing the
rejection); hosted-harness-session.test.ts 181/181. Nine mutation
witness arms all red (revert toJson canonicalization, forget idle
signal in the closed guard, drop the in-lock re-check, unbounded
cause-walk, blanket boot-header wrap at prompt and DELETE sites,
detach without finally, watchdog removed with the keepalive arm
staying green, ref-only resume fallback).

* fix(sdk-java): close third-round review criticals on the Hosted Harness client (#13314)

- submitTurn: a definitive unfenced refusal (a response without a valid
  boot header that is not an ambiguous 408/5xx) now releases only the
  active-prompt marker the call owns, in both the validateGeneration arm
  and the post-consume arm; a refusal produced before the route's
  promptId lookup can no longer delete an earlier same-identity entry
  whose outcome is unknown, and a refused fresh submission no longer
  wedges the session.
- send: snapshot the executor termination state before dispatch so a
  close() racing an in-flight send keeps its outcome-unknown
  classification instead of certifying non-dispatch.
- StreamHarnessEvents: an explicit lastEventId cursor now always wins,
  including a deliberate 0 (replay from the beginning), which
  HarnessCoordinator passes for turn rows without a recorded watermark;
  only an omitted cursor falls back to the session ref's watermark.
- HarnessEventStream: the idle watchdog now measures peer silence only
  while a consumer is parked in next(), so time between calls (a
  blocking queue handoff, a slow event handler) no longer aborts a
  healthy keepalived stream.
- docs: the payload digest is re-derived by the server from its own
  re-serialization of the parsed prompt member (JSON.stringify form),
  not from the received bytes; the README qualifies cross-release digest
  stability and the keepalive floor for a custom idle timeout.

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

* fix(sdk-java): close fourth-round review findings on the Hosted Harness client (#13314)

- Carry the wire status and machine-readable code through
  PromptAlreadyActiveException instead of collapsing every 409 into a
  fixed message
- Keep a prompt marker whose submission is still in flight from being
  cleared by a status answer that predates the admission
- Map a watchdog scheduling rejection to DaemonTransportException and
  drop the stranded stream registration
- Document the enforced idle-abort window (budget to 1.5x) and the
  peer-side backpressure limit on consumer pauses
- Pin the fenced definitive-refusal release, the Duration.ZERO watchdog
  opt-out, the next() entry stamp, and the terminated-executor release
  with dedicated regression tests

* fix(sdk-java): close fifth-round review findings on the Hosted Harness client (#13314)

- Retain the active-prompt marker when a fenced 409 carries
  hosted_prompt_recovery_required: that code proves the promptId is in
  the durable input log, so releasing the marker would admit a second
  identity into a session with two unsettled inputs and fail every
  recovery entrance closed; every other 409 code still releases it
- Clear the hosted events keepalive from stop() as well as from the
  close handler, and send X-Accel-Buffering: no so a buffering
  intermediary cannot swallow the keepalives the client watchdog relies
  on
- Pin the settled status-read clearing, the failed-close unregister, and
  the disabled watchdog's empty scheduler queue with positive witnesses,
  and extract the shared fenced-SSE response prelude in the client tests
- Correct the documented idle-abort window with the 100 ms poll floor,
  state the digest's sha256: wire encoding in the README, and narrow the
  PromptAlreadyActiveException javadoc to what the code does

* fix(sdk-java): lifecycle teardown runs only on definitive answers

Close the corridor the next-round review's close/detach receipts queued:
local attachments (and their heartbeat timers) must not survive a
definitive refusal, and must NOT be torn down on outcome-unknown.

- detachSession: the unconditional `finally { removeAttachment }` also
  ran when requireMutationStatus surfaced unexpected-success as
  MutationOutcomeUnknownException — a state where the server's answer is
  not decided and the lease must keep beating. Teardown now runs on the
  definitive limb (204 or tolerated 404) and inside the
  DaemonHttpException arm only; outcome-unknown surfaces keep the
  attachment (anUnexpectedDetachAnswerKeepsTheAttachment).
- closeSession(HarnessSessionRef) and closeSession(String): mirror the
  same try/catch — a definitive DELETE refusal (e.g. 403) now also retires
  the attachment and the active-prompt ledger instead of leaking the
  timer until close(), while ambiguous answers keep them
  (aRefusedCloseStillTearsDownTheHeartbeat ×2 overloads,
  anUnexpectedCloseAnswerKeepsTheAttachment,
  aBootHeaderlessCloseKeepsTheAttachment for the gateway-502 shape).
- closeSession(String) gains the ensureOpen() guard every other public
  entry has (a deferral receipt plus #13313's R1-50): the closed-client
  path is JVM-independent again, so aLocalExecutorRejectionIsNotReported
  AsOutcomeUnknown becomes closeSessionByIdFailsFastAfterClientClose —
  IllegalStateException on every JDK — and the saturated/terminated
  taxonomy stays pinned by aTerminatedHttpExecutorReleasesTheOwnedMarker.
- closeByRefTreatsAnAbsentHarnessSessionAsClosed now also counts the
  DELETE request (dropping the remote call reddens assertEquals(1)).
- Housekeeping of the takeover branch: aRecoveryRequiredRefusalKeepsThe
  OwnedMarker's stub never incremented its promptCalls counter, so its
  trailing assertion could never pass once it ran (CI never reached it
  through the verify-checkout race) — fixed by counting in the handler.

Witnesses: full module 214/214 green on JDK 17 (clean test green on JDK
11, 13 skips = 9 env + 4 version-gated); corridor mutation arms all red —
detach with an unconditional finally flattens the keep-beating assertion,
removing either close-mirror makes refused-close heartbeats keep growing
(9→14, 10→16), and removing ensureOpen flips the fail-fast test from
IllegalStateException back to the transport exception.

* fix(sdk-java): settle registered admission markers and align lifecycle teardown

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

* fix(sdk-java): retire hosted admission state on proven restart and scope the settle latch per attempt (#13314)

A proven harness generation change now retires every local record for
that peer at all three entrances: the submitTurn fence catch releases
the admission ledger entry, the scheduled heartbeat retires the
attachment and ledger instead of swallowing the mismatch, and
detachSession mirrors closeSession for the generation branch while
keeping the ledger across definitive refusals.

The admission settle latch is per-attempt and outcome-scoped: only the
owning call's concluded send settles it, every attempt re-arms it, and
a same-identity retry's 202 publishes a fresh settled entry so a status
read holding a stale snapshot cannot clear the marker that is live now.
Pre-dispatch unchecked escapes release the owned marker via a dispatch
boundary flag in send() instead of settle-and-keep.

The SSE idle watchdog merges the parked flag and activity stamp into
one volatile waitingSince write, so a tick can never pair a fresh flag
with a stale stamp; keepalive refills still refresh the budget.

Docs correct the persisted-digest claim: a form change invalidates rows
whose stored key order was not already canonical, not every row.

* fix(cli): drop ambient QWEN_HOME from the cli test setup (#13314)

A run from inside a Qwen Code session always exports QWEN_HOME, which
outranks the homedir() mocks the settings suite anchors on: the
module-load getUserSettingsPath() then resolves to the ambient home, the
mocked user-settings file is never found, and 13 settings.test.ts tests
fail. The verification gate runs in exactly such an environment. Delete
the variable in test-setup.ts, mirroring the existing QWEN_RUNTIME_DIR
sanitization; tests that exercise QWEN_HOME set it in-body.

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

* fix(sdk-java): close hosted harness ledger wedges from review (#13314)

Unfenced 2xx/3xx mutation answers are now outcome-unknown instead of
definitive, unfenced refusals carry the peer's buffered body, the
per-attempt settle latch is owner-gated, close/detach retire only the
records they observed, and wire 409s stay reachable through
DaemonHttpException.

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

* fix(sdk-java): close hosted harness review criticals on digest recovery, refusal typing, and ledger identity (#13314)

- SubmitHarnessTurn accepts a persisted pre-canonicalization payload
  digest and re-sends the prompt in the exact as-built key order that
  digest covers, so recovering an older turn row reaches the route's
  replay path instead of failing the self-check before any request is
  sent.
- An unfenced 4xx on the generic mutation channel surfaces as the
  protocol failure it is rather than DaemonHttpException, whose status
  consumers key terminal verdicts on; only submitTurn keeps the typed
  refusal its admission-marker release keys on.
- ActivePrompt records the submitting client id and replaces the shared
  settle latch with a per-attempt send counter: lifecycle teardowns
  retire only markers whose identity proved stale, and a status read
  clears a marker only while no send attempt is in flight.

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

* fix(sdk-java): close hosted harness round-10 review criticals on close/detach retirement (#13314)

- README: correct the digest-migration paragraph — the client re-sends
  the shape its digest covers and accepts a pre-canonicalization as-built
  digest, so upgrades do not quiesce or rewrite in-flight turn rows
- closeSession(String): a successful headerless DELETE deleted the
  session outright, so retire every attachment for the id, including one
  registered while the DELETE was in flight
- submitTurn: gate all six owning-release arms on no send attempt still
  open, so a definitive refusal cannot strand a same-identity retry's
  202 with no ledger entry
- detachLifecycle: retire the observed admission marker even when no
  attachment remains (a refused close drops the attachment first), in
  both overloads
- closeSession(HarnessSessionRef): a 204 proves identity-matched
  deletion, so retire every marker for the id; keep identity-matched
  retirement only for the tolerated identity-scoped 404

* fix(cli): wait out the record watcher's backoff in the unrecorded-reply squad test (#13314)

The deterministic verification gate rejected the previous commit on
"wakes the leader with the error when a member reply is never recorded"
(default 1s vi.waitFor timed out waiting for the leader's wake). The
give-up that wakes the leader is eligible the moment the test bumps its
virtual clock, but it fires on the watcher's next real-timer check, and
that chain backs off geometrically (recordWatchMs doubling to a 15s cap)
— under full-suite load the next tick lands past the 1s default. The
sibling restart-recovery test already guards the identical pattern with
timeout: 3_000 and the comment "The watcher backs off; its next check
may be a while."; apply the same bound here. Assertion-preserving.

Probe: with the watcher's initial interval raised to 1.5s, the
unmodified test fails with the gate's exact assertion at the same line;
with the timeout it passes.

* fix(sdk-java): close HostedHarnessClient admission-ledger fences found in review (#13314)

Round-14 review found the admission-ledger fencing still open in eight
places:

- detachLifecycle(HarnessSessionRef) retired the pre-send snapshot even
  on a tolerated 404, wiping the marker the session's current owner
  planted through a re-issued identity; it now retires through the same
  identity-matched retireLedger detachSession uses, which also covers a
  marker this identity replanted while the detach was in flight.
- detachSession collapsed a deleting 204 and a tolerated 404 into one
  identity-matched retirement, so a proven deletion kept a marker from
  an older identity; the 204 arm now retires unconditionally.
- Both closeSession overloads applied the observed-snapshot rule to a
  generation change, so a marker published while the DELETE was in
  flight survived a proven restart; both now retire unconditionally on
  HostedHarnessGenerationException.
- The sendMutation channel and the streamEvents fence observed a
  generation change without retiring local state; both now discard it,
  and sendMutation threads the session id from every caller.
- The unfenced definitive-4xx arm released without reading the refusal
  code; an unfenced 409 is now narrowed by code like the fenced arm, so
  a boot-header-stripped hosted_prompt_recovery_required retains the
  marker and surfaces as PromptAlreadyActiveException.
- When both the owning attempt and a joined same-identity retry were
  definitively refused, neither released the marker; an owning release
  refused by an open sibling send is now owed and discharged at the last
  concluded attempt, while outcome-unknown or durably admitted
  conclusions block it.
- A non-owning retry's send window closed before its 202 republish, so
  an owning release landing in between dropped the joined entry and the
  replace silently no-oped; the window now closes after the ledger
  decision.
- The dispatch guard probed the executor, so a saturated-but-running
  pool was misclassified outcome-unknown; send() now dispatches with
  sendAsync and maps the synchronous RejectedExecutionException to a
  known-outcome DaemonTransportException, keeping post-dispatch
  rejections outcome-unknown.

Each fix carries a pinning test, and the three pre-dispatch escape tests
move their injection seam from the removed executor-state probe to the
executor's execute().

* test(cli): settle fresh process rows before identity-judged ledger sweeps (#13314)

The deterministic gate rejected the previous round on
managed-runtime-ledger.test.ts: on some hosts ps reports a just-born
process with the negative-elapsed wraparound (e.g. 441077234-00:18:40,
which parsePsElapsed rightly rejects) until it ages past the glitch, so
an unwitnessed sweep judged the live fixture 'unknown' and held it
unproven instead of killing it. settledGroupTable now also waits for
the leader's row to carry a datable elapsed column, and the five
live-spawn sweep tests that never settled now settle before sweeping.
Assertions unchanged.

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

* fix(sdk-java): report a close-aborted parked SSE read as a transport failure (#13314)

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

* chore(sdk-java): re-verify #13314 on a tree clean of generated Maven artifacts

The deterministic gate rejected 253326ce58 because repo-wide
'npm run lint' (eslint flat config lints .js repo-wide) swept up the
JaCoCo report resources that local 'mvn test' generates under
packages/sdk-java/*/target/site/jacoco/ — 139 errors in
jacoco-resources/prettify.js and sort.js. Those files are git-ignored
build output, not committed content; the committed tree is lint-clean.

This commit carries no tree change: the repair is the cleaned working
tree, with 'npm run lint' now passing, and build/typecheck plus the
focused R15-1 contract tests (HarnessEventStreamCloseTest and the four
HostedHarnessClientTest idle-timeout/close cases) re-verified green.

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

* fix(sdk-java): close hosted harness ledger and SSE close races

- Classify a parked SSE read's raced close as DaemonTransportException,
  matching the interrupted-read signal, with the idle-timeout half kept
  distinct; the programming-error type stays reserved for callers
  arriving after the close.
- Keep the admission ledger on every refused close, on a settled release
  whose entry is releaseBlocked, and for a code-less unfenced 409, which
  is outcome-unknown rather than a definitive refusal.
- Retire the marker an owning same-identity replay just planted when the
  promptId's terminal event was already observed, via the settled-checked
  release path.
- Read the getStatus marker clock before settledness and re-check both
  inside the bin-locked clear, so a retry's beginSend landing between the
  snapshot reads cannot be cleared under its open send window.
- Retire only the observed attachment on a by-id close, and self-retire
  an attachment whose heartbeat meets a definitive
  hosted_session_not_found 404 instead of beating against the dead
  session forever.

* chore(sdk-java): repair round verification on a clean working tree

The deterministic gate rejected 4884dc8197 on npm run lint: the previous
round's mvn test had generated the git-ignored JaCoCo HTML report under
packages/sdk-java/qwencode/target/site/jacoco/jacoco-resources/*.js, and
the flat eslint config lints git-ignored-but-present .js files (it does
not import .gitignore), failing on the vendored prettify.js/sort.js. The
root eslint config is outside this PR's footprint, so the repair removes
the untracked build output from the working tree. No source change; the
rejected commit's content is preserved as-is.

* fix(sdk-java): retire hosted admission state on journal replays and deleting lifecycle detaches (#13314)

* docs(sdk-java): state the digest canonical key order the code implements (#13314)

The published digest contract claimed object keys are sorted
recursively, but JsonSupport.ownKeyOrder and the route's
sha256(JSON.stringify(prompt)) re-derivation use ECMAScript own-key
order: array-index keys ascend numerically first, then the remaining
keys in insertion order. A second-language client built on the
published sentence computes a different digest and is refused with
invalid_hosted_prompt.

* fix(sdk-java): keep teardown retirement off live state and durable history (#13314)

A detach or a DELETE closes only the live Hosted session: the durable
journal survives it, and after a re-load the prompt route answers a
settled promptId's same-identity replay with a 202 that owes no terminal
event. Retiring the terminal-event record on teardown therefore left
that replay's marker vetoing every later prompt. No teardown retires the
record any more; the map is bounded instead (least recently used past
4096 sessions), and an evicted session's replay marker is cleared by the
busy-turn status probe.

The headerless lifecycle detach also removed the admission marker
unconditionally after its attachment snapshot, so a re-load that
registered a new attachment and admitted a turn inside that window lost
its marker and a different prompt reached the wire. Every definitive
teardown answer (detach 204 on all three entrances, the headerless
tolerated 404, DELETE 204 on both overloads) now retires through one
bin-locked decision that reads the current attachment under the
marker's lock: a marker the current attachment's identity planted, or
joined with a same-identity retry, stays; an abandoned identity's marker
retires. The identity-scoped tolerated-404 retirement keeps a marker
another identity's retry joined, under the same lock.

Tests: the teardown cases are parameterized over all five entrances;
fixtures whose vetoed submit hit an unrouted /status now answer the
busy-turn probe instead of waiting out the request timeout (the class
drops from ~320 s to ~20 s), and the probe's generation-change and
send-clock behavior are pinned.

---------

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: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
2026-10-16 21:06:22 +08:00
haochen
008c0de746
docs(sdk): clarify empty core-tool allowlists across SDKs (#13949)
Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
2026-10-12 13:19:59 +00:00
jinye
585466c646
feat(sdk-java): Add the Hosted Harness private client (#12654)
Add the Java control-plane transport for the hosted Harness private
protocol from #12409. Construction performs capability negotiation,
validates the advertised capability digest against the configured
deployment digest, and pins the process boot identifier; every later
request then carries the protocol and boot-ID fencing automatically,
and a generation mismatch surfaces as a non-retryable conflict rather
than a retry against a restarted Harness. Sessions are created, loaded,
heartbeated, prompted, streamed, and cancelled through typed request
objects; turn submissions carry a precomputed payload digest, and event
streams reuse the daemon SSE reader with replay cursors.

The client has no production caller yet: it is the transport the Spring
control plane will wire in a later slice, in the same pattern as the
attestation client before process adoption.

Related to #12380.
2026-09-24 16:47:15 +00:00
Yu Zhang
8b72f4d456
fix(java): make SDK worker threads daemon (#9925)
* fix(java): make SDK worker threads daemon

Use daemon threads for the Java SDK default executors so background helper pools do not keep example or short-lived JVM processes alive after work completes.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(java): document and test daemon pool lifecycle

Keep examples and docs aligned with daemon executors, and ensure the client module actually discovers its JUnit 5 regression test.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(java): reuse the custom example executor

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* test(java): isolate SDK integration cases

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

---------

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
2026-09-01 18:06:25 +00:00
Yu Zhang
8104071260
docs(java): correct closed session exception (#9859)
* docs(java): correct closed session exception

* docs(java): clarify session control exceptions

Clarify which Java SDK entry points throw SessionControlException directly and pin closed-session behavior with a lightweight unit test.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* test(java): pin unavailable session exceptions

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* test(java): pin session exception wrapping

Add coverage for direct session initialization failures and both QwenCodeCli.newSession wrapping paths so the Java SDK docs stay aligned with the exception contract.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

---------

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
2026-08-29 15:01:02 +00:00
Yan Shen
867ded5bd5
fix(sdk): support "auto" permission mode (#9003)
* fix(sdk-python): support "auto" permission mode

* refactor(sdk-python): derive permission-mode validation from PermissionMode type

* fix(sdk-java): support "auto" permission mode

* refactor(sdk-python): derive auth-type and effort validation from type aliases

* chore: rerun CI
2026-08-22 14:25:40 +00:00
Dragon
e09399e0fb
feat(core): make list_directory opt-in (disabled by default) (#9424)
* feat(core): make list_directory opt-in (disabled by default)

glob covers directory listing in most cases, so list_directory is now disabled by default to keep the tool surface lean. Enable it with tools.listDirectory.enabled=true or by listing it in the coreTools allowlist (--core-tools / tools.core). The plan-mode reminder no longer steers the model toward the tool.

* fix(core): align list_directory opt-in gate with prompts and allowlist parsing

Round-1 review findings on the opt-in gate:

- Normalise coreTools entries with parseRule so specifier forms such as
  list_directory(/src) still re-enable the tool. isLsToolEnabled used
  resolveToolName, which does not strip specifiers, while
  PermissionManager admits the same entry via parseRule — so the
  allowlist accepted the tool and the registry never got it (R1-1).
- Drop list_directory from the plan-mode block error. That message is
  returned to the model as the tool result of every blocked call, so it
  kept steering the model at an unregistered tool even after the system
  prompt stopped doing so (R1-2).
- Remove the now-inert ToolNames.LS entries and the prompt text that
  advertised the tool to built-in agents. The gate also guards the
  forked-registry rebuild path, so these agents were silently stripped
  of a tool their own prompts told them to use (R1-3).
- Say why the tool is missing and how to enable it in the tool-not-found
  message, instead of offering unrelated Levenshtein suggestions (R1-6).
- Cover the alias and specifier forms in the registration tests (R1-4),
  and drop the stale examples from the sub-agents and Java SDK docs,
  where the listed entry can no longer register the tool (R1-5, R1-7).

* fix(core): address list_directory opt-in round-2 review findings

- Reword the skill-review agent task prompt so the inspection guidance
  only references read_file; the run's tool filter has no
  list_directory, glob, or shell, so the old `ls` instruction was
  unsatisfiable for the turn-budgeted background agent.
- Resolve tool-name aliases (ListFiles, ListFilesTool, ReadFolder) in
  the list_directory not-found message so aliased calls get the
  enablement explanation instead of a Levenshtein suggestion.
- Attribute a missing list_directory to the workspace tools toggle when
  the workspace disabledTools set blocks it, since the opt-in setting
  cannot lift that state.
- Drop the coreTools allowlist advice from the enablement message:
  setting tools.core to ["list_directory"] alone would exclude every
  other tool.
- Switch the two remaining sdk-java runTransportOptionsExample copies
  from list_directory to glob; setAllowedTools only adds auto-approve
  rules and never registers a tool.
- Extract the shared fake-server scaffolding in the list_directory
  integration tests into a local helper, keeping the load-bearing
  CLI-flag comment.
- Pin the skill-review agent tools array and assert the learn-skill
  prompt steers to read_file / glob, matching the sibling planners.

* fix(core): only claim list_directory is opt-in-disabled when it is unregistered

The not-found explanation resolved aliases (ListFiles, ReadFolder) before
checking whether the tool was actually absent. The registry is keyed by
canonical names while the lookup that reaches this path resolves legacy
migrations only, so an alias call missed even when list_directory was
enabled — and the message then told the user to switch on a setting that was
already on, hiding the generic path's "Did you mean list_directory"
self-correction.

Gate the branch on the canonical name being absent from the registry, and pin
the two alias combinations that were unpinned: an alias against a non-empty
workspace disabledTools set, and an alias against a registered tool.
2026-08-21 07:24:05 +00:00
jinye
60458f5e37
fix(serve): Coordinate caller-supplied session IDs (#8415)
* fix(serve): coordinate caller-supplied session IDs

Complete daemon-wide admission across REST, ACP, workspace generations, SDKs, and MCP.

Closes #8411

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

* test(serve): wire session bridges in hot-reload harness

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

* fix(serve): address review round for caller-supplied session IDs (#8415)

Restore the observability and fail-loud guarantees flagged in review:
log every session-id admission routing failure, name the live foreign
owner workspace in restore conflicts, make the ACP dispatcher's
admission dependency required so load/resume cannot run on a mount
without one, and require mountAcpHttp hosts to inject the daemon-wide
admission instead of silently building a weak fallback. Harden the SDK
WS transport against environments without global fetch and against
non-capabilities 200 envelopes, and align the design doc with the
implemented restore-sharing and persistence-failure semantics.

* fix(sdk): harden session ID capability fallback

Preserve REST capability errors, fail closed on malformed envelopes, retain restore routing diagnostics, and align retry documentation.

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

* fix(serve): normalize restored session IDs

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

* fix(session): preserve mixed-case legacy session access

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 07:31:30 +00:00
jinye
edfb43e954
fix(sdk-java): Harden daemon transport reliability (#7603)
* fix(sdk-java): propagate daemon event epochs

Pair SSE cursors with the daemon event epoch, learn validated response epochs, and fail closed when the epoch changes during prompt observation.

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

* fix(sdk-java): harden daemon reliability follow-ups

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

* test(sdk-java): cover duplicate SSE event epoch headers

* test(sdk-java): restore retryable admission coverage

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-24 04:22:05 +00:00
jinye
e7097d0ef6
feat(sdk-java): Add daemon transport (#7463)
* feat(sdk-java): add daemon transport

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

* codex: address PR review feedback (#7463)

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

* test(sdk-java): stabilize lifecycle lock test

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

* codex: address PR review feedback (#7463)

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

* codex: address PR review feedback (#7463)

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

* codex: address PR review feedback (#7463)

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

* fix(cli): preserve prompt cancellation during context propagation

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-23 12:38:34 +00:00
zhangxy-zju
ed14a33064
feat(core): add NotebookEdit tool for Jupyter notebooks
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / CodeQL (push) Blocked by required conditions
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
Adds NotebookEdit as the structured write counterpart to existing notebook read support.

Summary:
- Add `notebook_edit` for safe cell-level `.ipynb` replace/insert/delete operations.
- Integrate notebook editing with tool registration, permissions, Claude conversion, prior-read enforcement, IDE/inline modify flow, commit attribution, docs, and SDK permission docs.
- Harden notebook read/edit behavior for truncated notebook renders, ambiguous fallback cell IDs, internal modify metadata, compact JSON, UTF-8 BOM notebooks, and cache behavior after structural edits.
- Add unit and integration coverage for notebook read/edit behavior.

Follow-up work remains for tab-indented notebook formatting preservation, a few low-risk unit-test additions, and non-blocking hardening suggestions from review.
2026-05-21 00:06:15 +08:00
Viktor Szépe
a1d1e5e276
Fix typo in class name (#2189) 2026-04-18 11:59:36 +08:00
tanzhenxin
a4ffc6eb24 feat: promote Agent Skills from experimental to stable
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-06 13:04:08 +08:00
skyfire
0128f65fe8 refactor structure 2026-02-04 18:38:38 +08:00
Renamed from packages/sdk-java/README.md (Browse further)