@kanyun-ai-infra/agenthub 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4935,9 +4935,18 @@ var DeliveryResultArtifactItemSchema = import_zod42.z.object({
4935
4935
  /** Artifact category. Workers MAY add new kinds; storage is opaque. */
4936
4936
  kind: import_zod42.z.enum(["diff", "log", "general"]),
4937
4937
  /**
4938
- * Logical path inside the sandbox workspace (used as the idempotency
4939
- * key together with `runId`). Two writes with the same `(runId, path)`
4940
- * pair collapse to a single row via `ON CONFLICT DO NOTHING`.
4938
+ * Logical path inside the sandbox workspace, and the idempotency key of
4939
+ * the artifact row: `(runId, path)` on the run plane, `(sessionId, path)`
4940
+ * on the session plane. Two writes with the same pair collapse to a single
4941
+ * row — the run plane keeps the first (`ON CONFLICT DO NOTHING`), the
4942
+ * session plane keeps the newest bytes and moves `turn_id` to the newest
4943
+ * ATTRIBUTED writer (`ON CONFLICT DO UPDATE`, T-INV-5).
4944
+ *
4945
+ * Two items with the SAME path inside one request body are legal wire but
4946
+ * a single artifact: the session plane de-duplicates them before the
4947
+ * insert, last one wins, and `artifactsInserted` counts the de-duplicated
4948
+ * items. Not a 400 — see `turnId` below on why an artifact-shaped problem
4949
+ * must not be the reason a delivery result is lost.
4941
4950
  */
4942
4951
  path: import_zod42.z.string().min(1).max(1024),
4943
4952
  /** Object key inside the OSS bucket where the artifact was uploaded. */
@@ -4955,6 +4964,26 @@ var DeliveryResultArtifactItemSchema = import_zod42.z.object({
4955
4964
  contentType: import_zod42.z.string().min(1).max(255)
4956
4965
  }).strict();
4957
4966
  var DeliveryResultUpdateSchema = import_zod42.z.object({
4967
+ /**
4968
+ * The turn this delivery result belongs to — `ctx.turnId` on the worker
4969
+ * side (`deriveWorkerTurnId`, uuid v5). Drives `artifacts.turn_id`; see
4970
+ * `specs/features/pilot-v2-session-turns.md` §Projection 6.
4971
+ *
4972
+ * Optional through the migration window: a sandbox image predating P2
4973
+ * omits it, and the control plane then INFERS the turn and records an
4974
+ * anomaly rather than rejecting the write. Delivery attribution must not
4975
+ * become a reason a delivery fails — which is also why an id that names no
4976
+ * turn of THIS session degrades to the same inference (plus a warn log and
4977
+ * the anomaly naming the rejected id) instead of failing the request. The
4978
+ * id is still never honoured across sessions: `artifacts.turn_id` has no
4979
+ * foreign key, so a foreign id would attach these artifacts to a turn
4980
+ * another session's `GET /turns` serves.
4981
+ *
4982
+ * Deliberately NOT part of the `.refine()` below: it qualifies the other
4983
+ * fields rather than mutating anything, so a body carrying only `turnId`
4984
+ * is still the empty request it looks like, and still a 400.
4985
+ */
4986
+ turnId: import_zod42.z.string().uuid().optional(),
4958
4987
  /**
4959
4988
  * The PR / MR URL the agent created. First-write-wins per spec
4960
4989
  * (`UPDATE ... WHERE pull_request_url IS NULL`); subsequent writes
@@ -4963,10 +4992,11 @@ var DeliveryResultUpdateSchema = import_zod42.z.object({
4963
4992
  */
4964
4993
  pullRequestUrl: import_zod42.z.string().url().max(1024).optional(),
4965
4994
  /**
4966
- * Append-set of artifacts uploaded for this run. Each item is
4967
- * upserted independently via the `unique(run_id, path)` constraint;
4968
- * the response indicates how many of the submitted items resulted
4969
- * in a new row.
4995
+ * Append-set of artifacts uploaded for this run. Each item is upserted
4996
+ * independently via the `unique(run_id, path)` / `unique(session_id, path)`
4997
+ * constraint; the response indicates how many of the submitted items
4998
+ * resulted in a new row. Items are keyed by `path` — see the field's own
4999
+ * doc for what a repeated path inside one body means.
4970
5000
  */
4971
5001
  artifacts: import_zod42.z.array(DeliveryResultArtifactItemSchema).max(64).optional(),
4972
5002
  /**
@@ -5652,7 +5682,28 @@ var RecordWorkspaceSnapshotRequestSchema = import_zod50.z.object({
5652
5682
  * parts may be null ("nothing to restore" — a valid no-op snapshot, NOT
5653
5683
  * a failure, per 已定方案 决策②).
5654
5684
  */
5655
- snapshot: WorkspaceSnapshotSchema
5685
+ snapshot: WorkspaceSnapshotSchema,
5686
+ /**
5687
+ * The turn whose end produced this snapshot (`ctx.turnId`, uuid v5 from
5688
+ * `deriveWorkerTurnId`). The control plane stamps
5689
+ * `session_turns.workspace_snapshot_captured_at` on that row; the stored
5690
+ * snapshot JSON and its session-level latest-wins semantics are unchanged.
5691
+ *
5692
+ * **It lives on the ENVELOPE, not inside `snapshot`** (T-INV-6).
5693
+ * `WorkspaceSnapshotSchema` is `.strict()` and is re-used verbatim as
5694
+ * `PodAgentStartWorkerRequestSchema.workspaceSnapshot`, which travels
5695
+ * control-plane → pod-agent: a key added there makes every pod-agent
5696
+ * older than the change reject `POST /pilot/worker/start`. The contract
5697
+ * test in `__tests__/pilot-v2-workspace-snapshot.test.ts` pins this.
5698
+ *
5699
+ * Optional for one migration window. This envelope is `.strict()`, so a
5700
+ * worker that sends `turnId` to a control plane predating this field gets
5701
+ * a 400 and loses the snapshot entirely — **control-plane and agent-hub
5702
+ * MUST ship before the sandbox image**. Until the window closes the
5703
+ * control plane infers the turn (most recent completed one) when it is
5704
+ * absent; after it, this becomes required.
5705
+ */
5706
+ turnId: import_zod50.z.string().uuid().optional()
5656
5707
  }).strict();
5657
5708
  var RecordWorkspaceSnapshotResponseSchema = import_zod50.z.object({
5658
5709
  success: import_zod50.z.literal(true),
@@ -5685,44 +5736,54 @@ var WorkspaceExportPartSchema = WorkspaceSnapshotPartSchema.extend({
5685
5736
  var WorkspaceExportDataSchema = import_zod50.z.object({
5686
5737
  version: import_zod50.z.literal("v1"),
5687
5738
  /**
5688
- * Opaque monotonic cursor identifying this capture. **Not a turn ordinal**
5739
+ * **Internal ordering cursor. Prefer {@link WorkspaceExportDataSchema.shape.turnId}.**
5740
+ *
5741
+ * Opaque monotonic value identifying this capture. **Not a turn ordinal**
5689
5742
  * despite the name: the capture side sets it to `capturedAt.getTime()`
5690
5743
  * (epoch milliseconds, sandbox clock). The name is fixed by the stored
5691
5744
  * `WorkspaceSnapshot` envelope. Never render it as "turn N".
5692
5745
  *
5693
- * Its one job for a caller is deciding **whether the snapshot is this turn's
5694
- * or the previous turn's**. The worker emits the turn's terminal event
5695
- * BEFORE it captures and uploads, so an export issued the moment a turn
5696
- * completes races the capture: it returns either 404 or the PRIOR turn's
5697
- * snapshot. Compare against `completedAt` from the terminal event payload —
5698
- * both come from the same in-sandbox clock, and the capture is ordered after
5699
- * the terminal append — and poll until
5700
- * `capturedAtTurn >= Date.parse(completedAt)`, with a bounded timeout.
5701
- *
5702
- * **`>=`, not `>`.** The two readings are `ctx.now()` calls in one process
5703
- * separated by a single `await`, at millisecond resolution, so they can land
5704
- * on the SAME millisecond. Under a strict `>` that turn's own snapshot never
5705
- * satisfies the test, and the caller polls out the timeout and concludes "no
5706
- * files" for a turn that did produce them — a wrong answer that looks like a
5707
- * normal one.
5708
- *
5709
- * `>=` cannot admit the previous turn's snapshot: that capture happens in the
5710
- * tail of turn N-1, before turn N is dispatched at all, so it precedes turn
5711
- * N's `completedAt` by a whole turn (turns are serial per session — a second
5712
- * concurrent turn is refused with `SESSION_TURN_IN_PROGRESS`). Equality is
5713
- * only reachable within one turn, between its own terminal and its own
5714
- * capture.
5715
- *
5716
- * Residual limit, deliberately not papered over: if the sandbox clock steps
5717
- * BACKWARDS between the terminal append and the capture (NTP correction),
5718
- * `capturedAtTurn` lands before `completedAt` and neither `>` nor `>=`
5719
- * accepts it. The poll then times out, and the caller cannot distinguish that
5720
- * from "nothing was produced". Anchoring the snapshot to a turn identity
5721
- * rather than a timestamp is the real fix — see the spec's follow-ups.
5746
+ * Its remaining job is the platform's own latest-wins comparison on the
5747
+ * recording path. The freshness question it used to answer for callers —
5748
+ * "is this snapshot my turn's or the previous turn's?" — is now answered
5749
+ * directly by `turnId`: compare it against the turn you dispatched instead
5750
+ * of comparing timestamps. The old recipe (poll until
5751
+ * `capturedAtTurn >= Date.parse(completedAt)`, `>=` and not `>` because the
5752
+ * two `ctx.now()` readings can land on the same millisecond) still works and
5753
+ * is the only option against a sandbox image predating P2, but it carries a
5754
+ * failure mode `turnId` does not: a BACKWARD sandbox clock step (NTP
5755
+ * correction) between the terminal append and the capture puts
5756
+ * `capturedAtTurn` before `completedAt`, and neither `>` nor `>=` accepts
5757
+ * it — the poll then times out and looks exactly like "nothing was
5758
+ * produced".
5759
+ *
5760
+ * The race itself is unchanged either way: the worker emits the turn's
5761
+ * terminal event BEFORE it captures and uploads, so an export issued the
5762
+ * moment a turn completes returns either 404 or the PRIOR turn's snapshot.
5763
+ * Poll with a bounded timeout; with `turnId` the predicate is equality
5764
+ * against your own turn, so there is nothing to reason about clocks.
5722
5765
  */
5723
5766
  capturedAtTurn: import_zod50.z.number().int().nonnegative(),
5724
5767
  /** ISO-8601 capture time — the field to show a human. */
5725
5768
  capturedAt: import_zod50.z.string().min(1),
5769
+ /**
5770
+ * The turn whose end produced this snapshot, when the platform knows it.
5771
+ * This is the field a caller polls on: the export is this turn's iff
5772
+ * `turnId` equals the turn id it dispatched — no clock comparison, and
5773
+ * immune to the backward-clock-step hole in `capturedAtTurn`.
5774
+ *
5775
+ * Absent (or attributed to an earlier turn than you expect) in three cases,
5776
+ * all of which mean "not yours yet", not "broken":
5777
+ * 1. the snapshot was recorded by a sandbox image predating the envelope
5778
+ * `turnId` — the platform then attributes it to the most recent
5779
+ * completed turn and flags the inference;
5780
+ * 2. the turn's projection had not materialized its `session_turns` row
5781
+ * when the snapshot landed, so there was no row to stamp;
5782
+ * 3. the session predates this field entirely.
5783
+ * Keep the `capturedAtTurn` fallback for those; do not treat a missing
5784
+ * `turnId` as an error.
5785
+ */
5786
+ turnId: import_zod50.z.string().uuid().nullable().optional(),
5726
5787
  parts: import_zod50.z.object({
5727
5788
  repoBundle: WorkspaceExportPartSchema.nullable(),
5728
5789
  externalArchive: WorkspaceExportPartSchema.nullable()
@@ -7536,7 +7597,35 @@ var TurnArtifactBundleSchema = import_zod58.z.object({
7536
7597
  /** Absent for a `rejected` bundle — nothing was uploaded, so there is no row. */
7537
7598
  artifactId: import_zod58.z.string().optional(),
7538
7599
  state: ArtifactBundleStateSchema,
7600
+ /**
7601
+ * MUST STAY A PLAIN `number` — widening it to `.nullable()` is a wire break,
7602
+ * not an additive change. `@kanyun-ai-infra/agenthub` bundles contracts into
7603
+ * its published artifact (`noExternal: ["@pilot/contracts"]`), so SDK 1.6.0
7604
+ * carries a FROZEN copy of this schema with `entryCount` a required number.
7605
+ * Old readers tolerate keys they have never heard of (every envelope on the
7606
+ * turn read path is a non-strict `z.object`, which strips them) but not a
7607
+ * value outside the type they froze: a `null` here makes 1.6.0's
7608
+ * `sessions.turns.get` / `.list` throw `MALFORMED_PLATFORM_RESPONSE` for
7609
+ * every turn that has any artifact row at all.
7610
+ *
7611
+ * So "not counted" travels as `0` plus {@link entryCountKnown} `false`
7612
+ * rather than as `null`. The real count lives in the manifest inside the zip
7613
+ * in object storage, and the read path that produces a `Turn` does not fan
7614
+ * out to object storage to get it; to see the actual entries, follow
7615
+ * `TurnArtifacts.incrementalHref` / `cumulativeHref` on the parent object.
7616
+ */
7539
7617
  entryCount: import_zod58.z.number().int().nonnegative(),
7618
+ /**
7619
+ * Is {@link entryCount} a measurement? Absent or `false` means it is not —
7620
+ * the `0` above is a placeholder, NOT an empty bundle. Optional so the key
7621
+ * itself is additive: a reader frozen before it existed drops it and still
7622
+ * parses, and one that has it must treat "absent" as "unknown" too, because
7623
+ * that is exactly what a pre-P2 control plane sends.
7624
+ *
7625
+ * `true` is reserved for a build that really counts the manifest entries.
7626
+ * Nothing sets it today.
7627
+ */
7628
+ entryCountKnown: import_zod58.z.boolean().optional(),
7540
7629
  createdAt: import_zod58.z.string(),
7541
7630
  reason: import_zod58.z.string().optional()
7542
7631
  });
@@ -10654,14 +10743,14 @@ var SessionsResource = class {
10654
10743
  * **`waitForTurn` resolving does NOT mean the snapshot is ready.** The worker
10655
10744
  * emits the turn's terminal event and only then bundles, uploads and reports.
10656
10745
  * Exporting the instant a turn completes therefore returns either a 404 or —
10657
- * worse, because it is silent — the PREVIOUS turn's snapshot. Poll for
10658
- * freshness instead of calling once:
10746
+ * worse, because it is silent — the PREVIOUS turn's snapshot. Poll until the
10747
+ * export names your turn instead of calling once:
10659
10748
  *
10660
10749
  * ```ts
10661
10750
  * const { terminalEvent } = await agenthub.sessions.waitForTurn(sessionId, turnId);
10662
- * // `completedAt` exists ONLY on a success terminal, and is the same
10663
- * // in-sandbox clock as capturedAtTurn (capture is strictly ordered after the
10664
- * // terminal append). `payload` is typed `unknown`, hence the cast.
10751
+ * // `completedAt` exists ONLY on a success terminal. Its value is the
10752
+ * // fallback cursor below; its PRESENCE is what tells you the turn succeeded.
10753
+ * // `payload` is typed `unknown`, hence the cast.
10665
10754
  * const completedAtRaw = (terminalEvent.payload as { completedAt?: string })?.completedAt;
10666
10755
  * if (!completedAtRaw) {
10667
10756
  * // Failed / aborted turn: nothing was captured, and polling would just
@@ -10674,11 +10763,24 @@ var SessionsResource = class {
10674
10763
  * while (Date.now() < deadline) {
10675
10764
  * try {
10676
10765
  * const candidate = await agenthub.sessions.exportWorkspace(sessionId);
10677
- * // `>=`, not `>`: both timestamps are `ctx.now()` in the same sandbox
10678
- * // process, one `await` apart, at millisecond resolution — so they can
10679
- * // be EQUAL, and a strict `>` would then never accept this turn's own
10680
- * // snapshot and report "no files" after the full timeout.
10681
- * if (candidate.capturedAtTurn >= completedAt) { exported = candidate; break; }
10766
+ * // The snapshot names its own turn — compare ids, no clock reasoning.
10767
+ * if (candidate.turnId === turnId) { exported = candidate; break; }
10768
+ * // Fallback: `turnId` didn't match — either it's absent (session
10769
+ * // predates the field, or the turn's `session_turns` row hadn't
10770
+ * // materialized yet when the snapshot landed) or it's present but
10771
+ * // WRONG: a sandbox image older than this field makes the platform
10772
+ * // infer "most recent completed turn", which can land on a turn you
10773
+ * // didn't dispatch. Either way `turnId` cannot tell you "not mine" —
10774
+ * // only the timestamp can, so any non-match falls through to it, not
10775
+ * // just a null one. `>=`, not `>`: both timestamps are `ctx.now()` in
10776
+ * // the same sandbox process, one `await` apart, at millisecond
10777
+ * // resolution — so they can be EQUAL, and a strict `>` would never
10778
+ * // accept this turn's own snapshot, reporting "no files" after the
10779
+ * // full timeout.
10780
+ * if (candidate.capturedAtTurn >= completedAt) {
10781
+ * exported = candidate;
10782
+ * break;
10783
+ * }
10682
10784
  * } catch (err) {
10683
10785
  * // Discriminate on `code`, NOT on status: SESSION_NOT_FOUND is a 404 too,
10684
10786
  * // and a bad id / deleted session / lost access must surface immediately
@@ -10695,6 +10797,20 @@ var SessionsResource = class {
10695
10797
  * // exported === null → this turn produced no files. Normal, not a failure.
10696
10798
  * ```
10697
10799
  *
10800
+ * **`turnId` is the primary check; the timestamp branch is only a fallback.**
10801
+ * They are not equivalent. `turnId` is an equality test against an id you
10802
+ * already hold — but a non-matching `turnId` does not mean "not my turn": it
10803
+ * can be null, or it can be a DIFFERENT turn the platform inferred during the
10804
+ * migration window, and nothing in the response distinguishes the two. Treat
10805
+ * every non-match the same way and fall back to the timestamp rather than
10806
+ * special-casing "absent." `capturedAtTurn` is an internal epoch-ms cursor
10807
+ * with a failure mode `turnId` does not have: a BACKWARD sandbox clock step
10808
+ * (NTP correction) between the terminal append and the capture puts it before
10809
+ * `completedAt`, no comparison accepts it, and the poll times out —
10810
+ * reporting "no files" for a turn that produced them, which reads like a
10811
+ * normal result. Never display either value; `capturedAt` is the
10812
+ * human-facing field.
10813
+ *
10698
10814
  * **404 `WORKSPACE_SNAPSHOT_NOT_FOUND` is not by itself an error.** It throws
10699
10815
  * (a `PilotPlatformApiError`, so "this session never ran" is never silently
10700
10816
  * flattened to an empty result), and you MUST catch it inside the polling