@stigmer/runner 3.40.1 → 3.41.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/README.md +3 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/activities/call-agent.js +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -1
- package/dist/activities/discover-mcp-server.js +13 -29
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
- package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
- package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
- package/dist/activities/execute-cursor/approval-policy.js +11 -35
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/boundary-rows.js +4 -4
- package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
- package/dist/activities/execute-cursor/translator.d.ts +12 -17
- package/dist/activities/execute-cursor/translator.js +13 -30
- package/dist/activities/execute-cursor/translator.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -2
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
- package/dist/activities/execute-deep-agent/platform-route.js +87 -19
- package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
- package/dist/activities/execute-deep-agent/translator.js +5 -4
- package/dist/activities/execute-deep-agent/translator.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
- package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.js +7 -7
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
- package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
- package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
- package/dist/activities/execute-deep-agent/turn.js +15 -6
- package/dist/activities/execute-deep-agent/turn.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +2 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/exit-backstop.d.ts +64 -0
- package/dist/exit-backstop.js +79 -0
- package/dist/exit-backstop.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +12 -5
- package/dist/harness/terminal-table.js +13 -13
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/harness/transcript/builder.d.ts +21 -0
- package/dist/harness/transcript/builder.js +53 -31
- package/dist/harness/transcript/builder.js.map +1 -1
- package/dist/harness/transcript/events.d.ts +22 -26
- package/dist/harness/transcript/events.js +6 -5
- package/dist/harness/transcript/events.js.map +1 -1
- package/dist/harness/turn-context.d.ts +3 -1
- package/dist/harness/turn-context.js +5 -3
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +10 -4
- package/dist/main.js.map +1 -1
- package/dist/middleware/cost-advisory.d.ts +6 -3
- package/dist/middleware/cost-advisory.js +6 -3
- package/dist/middleware/cost-advisory.js.map +1 -1
- package/dist/middleware/execution-budget.d.ts +22 -7
- package/dist/middleware/execution-budget.js +57 -35
- package/dist/middleware/execution-budget.js.map +1 -1
- package/dist/middleware/index.d.ts +10 -7
- package/dist/middleware/index.js +10 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/types.d.ts +6 -1
- package/dist/runner-manager.js +3 -2
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +25 -0
- package/dist/runner.js +41 -14
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +23 -13
- package/dist/shared/approval-policy.js +47 -14
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +4 -5
- package/dist/shared/caller-identity.js +4 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +3 -2
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +2 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +10 -1
- package/dist/shared/mcp-resolver.js +11 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/memory-attachment.js +3 -2
- package/dist/shared/memory-attachment.js.map +1 -1
- package/dist/shared/model-error.d.ts +25 -3
- package/dist/shared/model-error.js +78 -10
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/platform-server-address.d.ts +71 -0
- package/dist/shared/platform-server-address.js +103 -0
- package/dist/shared/platform-server-address.js.map +1 -0
- package/dist/shared/synthesized-attachment.d.ts +0 -5
- package/dist/shared/synthesized-attachment.js +0 -12
- package/dist/shared/synthesized-attachment.js.map +1 -1
- package/dist/shared/tool-rounds.d.ts +51 -23
- package/dist/shared/tool-rounds.js +67 -24
- package/dist/shared/tool-rounds.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +15 -0
- package/dist/shared/workspace/platform-dir.js +18 -1
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +21 -14
- package/dist/shared/workspace/stigmer-link.js +91 -29
- package/dist/shared/workspace/stigmer-link.js.map +1 -1
- package/dist/tools/url-guard.d.ts +5 -1
- package/dist/tools/url-guard.js +9 -2
- package/dist/tools/url-guard.js.map +1 -1
- package/dist/worker.d.ts +11 -2
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
- package/dist/workflow-engine/tasks/human-input.js +79 -37
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +14 -4
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/engine-core.d.ts +25 -0
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +3 -0
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +9 -12
- package/dist/workflows/human-input-orchestrator.js +12 -44
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +1 -0
- package/src/__test-utils__/execution-record-fixture.ts +2 -1
- package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
- package/src/__test-utils__/test-gate.ts +8 -0
- package/src/__tests__/exit-backstop.test.ts +91 -0
- package/src/__tests__/golden-e2e.test.ts +80 -62
- package/src/__tests__/history-encryption-e2e.test.ts +51 -27
- package/src/__tests__/runner.test.ts +60 -2
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
- package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
- package/src/activities/call-agent.ts +1 -1
- package/src/activities/discover-mcp-server.ts +27 -37
- package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
- package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
- package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
- package/src/activities/execute-cursor/approval-policy.ts +11 -36
- package/src/activities/execute-cursor/boundary-rows.ts +3 -3
- package/src/activities/execute-cursor/translator.ts +15 -32
- package/src/activities/execute-cursor/turn-stream.ts +4 -1
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
- package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
- package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
- package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
- package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
- package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
- package/src/activities/execute-deep-agent/platform-route.ts +109 -20
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
- package/src/activities/execute-deep-agent/translator.ts +5 -4
- package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
- package/src/activities/execute-deep-agent/turn-setup.ts +7 -7
- package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
- package/src/activities/execute-deep-agent/turn.ts +16 -7
- package/src/activities/workflow-event-activities.ts +2 -0
- package/src/config.ts +11 -0
- package/src/exit-backstop.ts +97 -0
- package/src/harness/README.md +5 -5
- package/src/harness/terminal-table.ts +13 -13
- package/src/harness/transcript/__tests__/builder.test.ts +93 -21
- package/src/harness/transcript/builder.ts +53 -30
- package/src/harness/transcript/events.ts +22 -27
- package/src/harness/turn-context.ts +5 -3
- package/src/main.ts +9 -5
- package/src/middleware/__tests__/execution-budget.test.ts +181 -103
- package/src/middleware/cost-advisory.ts +6 -3
- package/src/middleware/execution-budget.ts +60 -34
- package/src/middleware/index.ts +10 -7
- package/src/middleware/types.ts +6 -1
- package/src/runner-manager.ts +3 -2
- package/src/runner.ts +57 -15
- package/src/shared/__tests__/approval-policy.test.ts +41 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/connect-backfill.test.ts +15 -11
- package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
- package/src/shared/__tests__/model-error.test.ts +71 -3
- package/src/shared/__tests__/platform-server-address.test.ts +122 -0
- package/src/shared/__tests__/tool-rounds.test.ts +64 -14
- package/src/shared/approval-policy.ts +57 -14
- package/src/shared/caller-identity.ts +4 -5
- package/src/shared/channel-attachment.ts +2 -2
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-resolver.ts +11 -1
- package/src/shared/memory-attachment.ts +2 -2
- package/src/shared/model-error.ts +87 -9
- package/src/shared/platform-server-address.ts +123 -0
- package/src/shared/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- package/src/shared/workspace/stigmer-link.ts +99 -29
- package/src/tools/__tests__/url-guard.test.ts +19 -1
- package/src/tools/url-guard.ts +9 -1
- package/src/worker.ts +12 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
- package/src/workflow-engine/__tests__/loader.test.ts +1 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
- package/src/workflow-engine/tasks/human-input.ts +92 -42
- package/src/workflow-engine/types.ts +16 -4
- package/src/workflows/call-agent-orchestrator.ts +7 -0
- package/src/workflows/engine-core.ts +18 -0
- package/src/workflows/execute-from-execution.ts +2 -1
- package/src/workflows/human-input-orchestrator.ts +15 -56
package/src/harness/README.md
CHANGED
|
@@ -200,11 +200,10 @@ Every hit falls into one of three classes:
|
|
|
200
200
|
| `apis/ai/stigmer/agentic/agentexecution/v1/enum.proto` | A comment on tool-name casing per harness. |
|
|
201
201
|
| `backend/README.md` | Lists the harnesses and the SDK behind each. |
|
|
202
202
|
| `client-apps/desktop/scripts/stage-runner-slim.sh`, `client-apps/desktop/scripts/macos-codesign-tree.sh` | Stage and sign the vendor SDKs' native helpers for the desktop app; a harness whose SDK ships one touches both. |
|
|
203
|
-
| `sdk/react/src/models/harness.ts` | The SDK hub: `HarnessOption`, `HARNESS_META`, `HARNESS_LABELS`, `HARNESS_OPTIONS`, `DEFAULT_HARNESS`, `toProtoHarness`, `fromProtoHarness
|
|
204
|
-
| `sdk/react/src/models/HarnessSelector.tsx` |
|
|
205
|
-
| `sdk/react/src/models/ModelSelector.tsx` | A Cursor-only thinking rule
|
|
203
|
+
| `sdk/react/src/models/harness.ts` | The SDK hub: `HarnessOption`, `HARNESS_META`, `HARNESS_LABELS`, `HARNESS_OPTIONS`, `DEFAULT_HARNESS`, `isHarnessOption`, `toProtoHarness`, `fromProtoHarness`, all read from one option-to-enum table; its reverse is typed over the whole enum, so a new member fails the SDK's typecheck here. |
|
|
204
|
+
| `sdk/react/src/models/HarnessSelector.tsx` | The premium mark on the Cursor segment; the segments themselves come from `HARNESS_OPTIONS`. |
|
|
205
|
+
| `sdk/react/src/models/ModelSelector.tsx` | A Cursor-only thinking rule. |
|
|
206
206
|
| `sdk/react/src/identity-account/AccountPreferencesPanel.tsx` | One `HarnessOptionRow` per harness, placed by hand. |
|
|
207
|
-
| `sdk/react/src/identity-account/useAccountExecutionDefaults.ts` | Validates the preference against the two names. |
|
|
208
207
|
| `sdk/react/src/workflow/inspector/forms/AgentCallForm.tsx` | Two radio buttons, placed by hand. |
|
|
209
208
|
| `sdk/react/src/pricing-governance/BaselineEditor.tsx` | A select with one option per harness. |
|
|
210
209
|
| `site/src/components/pages/pricing/ModelPricingTable.tsx` | Harness to display label. |
|
|
@@ -234,7 +233,8 @@ Nothing to touch. Listed so a probe's hit here is a stop, not a lead.
|
|
|
234
233
|
(read the session's harness or name the activities in prose).
|
|
235
234
|
- The React SDK: `sdk/react/src/models/registry.ts`,
|
|
236
235
|
`sdk/react/src/models/useModelRegistry.ts`,
|
|
237
|
-
`sdk/react/src/models/service-tier.ts
|
|
236
|
+
`sdk/react/src/models/service-tier.ts`,
|
|
237
|
+
`sdk/react/src/identity-account/useAccountExecutionDefaults.ts`; everything under
|
|
238
238
|
`sdk/react/src/channel/`, `sdk/react/src/composer/`,
|
|
239
239
|
`sdk/react/src/execution/`, `sdk/react/src/schedule/`,
|
|
240
240
|
`sdk/react/src/session/` (typed by the option or defaulting to one value).
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* harness once kept one function per arm (`streaming-terminal.ts`, retired
|
|
12
12
|
* in #1096); since #1096 both harnesses' turns end through this one table.
|
|
13
13
|
*
|
|
14
|
+
* The `error` of a FAILED arm is the failure itself, never framed: the
|
|
15
|
+
* `Execution failed:` wording belongs to the rows and to each consumer's
|
|
16
|
+
* own copy (the console's badge, the CLI's epilogue), so a consumer meets
|
|
17
|
+
* one grammar whichever arm wrote it. The interruption arms' `Execution
|
|
18
|
+
* interrupted:` errors are a different outcome, one the user resumes.
|
|
19
|
+
*
|
|
14
20
|
* The throw-vs-return rule, unchanged from the orchestrator it replaces:
|
|
15
21
|
* - RETURN when a Temporal retry would only repeat the outcome — a stall
|
|
16
22
|
* would wedge again, an exhausted budget would burn again, a platform
|
|
@@ -293,21 +299,15 @@ export function failedArm(message: string, surface: FailureSurface): TerminalArm
|
|
|
293
299
|
|
|
294
300
|
/**
|
|
295
301
|
* An error the runtime caught itself, outside any adapter (a resolution
|
|
296
|
-
* phase, the epilogue):
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
302
|
+
* phase, the epilogue): an `internal` failure like any adapter's, described
|
|
303
|
+
* as `[<type>] <message>` (`describeExecutionError` supplies the two parts),
|
|
304
|
+
* so `status.error` reads the same whichever side caught the fault. Until
|
|
305
|
+
* stigmer#1122 this arm alone prefixed `status.error` with `Execution
|
|
306
|
+
* failed:`, and a consumer that adds its own failure copy (the CLI's
|
|
307
|
+
* epilogue) printed the words twice.
|
|
301
308
|
*/
|
|
302
309
|
export function unexpectedErrorArm(errorType: string, errorMessage: string): TerminalArm {
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
306
|
-
error: `Execution failed: ${described}`,
|
|
307
|
-
rows: [TERMINAL_COPY.internalFailure.row, `Error details: ${described}`],
|
|
308
|
-
completes: true,
|
|
309
|
-
disposition: RETURN,
|
|
310
|
-
};
|
|
310
|
+
return failedArm(`[${errorType}] ${errorMessage}`, "internal");
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
// ── Applying an arm ─────────────────────────────────────────────────────────
|
|
@@ -19,10 +19,13 @@
|
|
|
19
19
|
* rows, finalize, artifacts and write-backs, the dirty flag, the error
|
|
20
20
|
* guard — joined commit by commit in the same PR. The observer arm (the
|
|
21
21
|
* runtime's fold-watcher: told after the fold, guarded apart from the
|
|
22
|
-
* handlers, silent on seed) joined with the turn timeline.
|
|
22
|
+
* handlers, silent on seed) joined with the turn timeline. The
|
|
23
|
+
* one-observation arms (`applyObservation`: one clock read for every stamp a
|
|
24
|
+
* raw event's facts make) joined with stigmer#1390, whose Cursor flake they
|
|
25
|
+
* make deterministic by moving the clock between folds.
|
|
23
26
|
*/
|
|
24
27
|
|
|
25
|
-
import { describe, it, expect } from "vitest";
|
|
28
|
+
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
|
|
26
29
|
import { create } from "@bufbuild/protobuf";
|
|
27
30
|
import { type AgentExecutionStatus, AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
28
31
|
import {
|
|
@@ -124,21 +127,18 @@ describe("TranscriptBuilder — tool_finished and tool_error are upserts", () =>
|
|
|
124
127
|
expect(row.error).toBe("EACCES");
|
|
125
128
|
});
|
|
126
129
|
|
|
127
|
-
it("
|
|
128
|
-
const {
|
|
130
|
+
it("an error alone writes no approval fact — a denied call is an ordinary failure until a proposal parks it (#1117)", () => {
|
|
131
|
+
const { status } = feed(builder(), [
|
|
129
132
|
{ kind: "message_start", runId: "run-1" },
|
|
130
133
|
{ kind: "text_delta", runId: "run-1", text: "Running the build." },
|
|
131
|
-
{ kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: "",
|
|
134
|
+
{ kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: "", provenance: "builtin_category" },
|
|
132
135
|
{ kind: "tool_error", callId: "sh-1", message: "Blocked by hook" },
|
|
133
136
|
]);
|
|
134
137
|
const row = rowOf(status, "sh-1");
|
|
135
|
-
expect(row.requiresApproval).toBe(true);
|
|
136
|
-
expect(row.approvalMessage).toBe("Run command: make");
|
|
137
138
|
expect(row.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
|
|
138
|
-
|
|
139
|
-
expect(
|
|
140
|
-
|
|
141
|
-
expect(rowOf(status, "sh-1").approvalRequestedAt, "stamped once").toBe(stamped);
|
|
139
|
+
expect(row.requiresApproval).toBe(false);
|
|
140
|
+
expect(row.approvalMessage).toBe("");
|
|
141
|
+
expect(row.approvalRequestedAt).toBe("");
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
it("an ungated row's error stamps no approvalRequestedAt", () => {
|
|
@@ -152,12 +152,11 @@ describe("TranscriptBuilder — tool_finished and tool_error are upserts", () =>
|
|
|
152
152
|
{ kind: "tool_finished", callId: "obs-1", result: "", observedAt: "2026-01-01T00:00:07.000Z" },
|
|
153
153
|
]);
|
|
154
154
|
expect(rowOf(status, "obs-1").completedAt, "the delta's own instant").toBe("2026-01-01T00:00:07.000Z");
|
|
155
|
-
sb.apply({ kind: "tool_started", callId: "obs-2", name: "Shell", input: { command: "make" }, mcpServerSlug: ""
|
|
155
|
+
sb.apply({ kind: "tool_started", callId: "obs-2", name: "Shell", input: { command: "make" }, mcpServerSlug: "" });
|
|
156
156
|
sb.apply({ kind: "tool_error", callId: "obs-2", message: "", observedAt: "2026-01-01T00:00:09.000Z" });
|
|
157
|
-
const
|
|
158
|
-
expect(
|
|
159
|
-
expect(
|
|
160
|
-
expect(gated.approvalRequestedAt, "the gate's stamp shares the observed instant").toBe("2026-01-01T00:00:09.000Z");
|
|
157
|
+
const failed = rowOf(status, "obs-2");
|
|
158
|
+
expect(failed.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
|
|
159
|
+
expect(failed.completedAt).toBe("2026-01-01T00:00:09.000Z");
|
|
161
160
|
// The stream's own error, arriving later with the text, fills the message
|
|
162
161
|
// and moves no stamp.
|
|
163
162
|
sb.apply({ kind: "tool_error", callId: "obs-2", message: "blocked" });
|
|
@@ -174,6 +173,75 @@ describe("TranscriptBuilder — tool_finished and tool_error are upserts", () =>
|
|
|
174
173
|
});
|
|
175
174
|
});
|
|
176
175
|
|
|
176
|
+
describe("TranscriptBuilder — one observation is folded at one instant", () => {
|
|
177
|
+
const T0 = "2026-01-01T00:00:00.000Z";
|
|
178
|
+
|
|
179
|
+
// The clock moves 1 ms after every fold, so any stamp that read it twice
|
|
180
|
+
// within one observation would show two instants.
|
|
181
|
+
function ticking(): Fold {
|
|
182
|
+
const status = create(AgentExecutionStatusSchema, {});
|
|
183
|
+
return { sb: new TranscriptBuilder("exec-instant", status, () => vi.advanceTimersByTime(1)), status };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
188
|
+
vi.setSystemTime(new Date(T0));
|
|
189
|
+
});
|
|
190
|
+
afterEach(() => {
|
|
191
|
+
vi.useRealTimers();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("every stamp of the batch is one instant: a row's start and finish, a sub-agent's open and close, the child's rows and messages", () => {
|
|
195
|
+
const { sb, status } = ticking();
|
|
196
|
+
sb.applyObservation([
|
|
197
|
+
{ kind: "message_finish", runId: "run-1" },
|
|
198
|
+
{ kind: "sub_agent_started", subAgentId: "task-1", name: "helper", subject: "Look.", input: "Look." },
|
|
199
|
+
{ kind: "tool_started", callId: "task-1", name: "task", input: { subagent_type: "helper" }, mcpServerSlug: "" },
|
|
200
|
+
{ kind: "tool_finished", callId: "task-1", result: "found" },
|
|
201
|
+
{ kind: "message_start", runId: "sub-1", subAgentId: "task-1" },
|
|
202
|
+
{ kind: "text_delta", runId: "sub-1", text: "Searching.", subAgentId: "task-1" },
|
|
203
|
+
{ kind: "tool_started", callId: "g", name: "grep", input: { pattern: "x" }, mcpServerSlug: "", subAgentId: "task-1" },
|
|
204
|
+
{ kind: "tool_finished", callId: "g", result: "x.ts", subAgentId: "task-1" },
|
|
205
|
+
{ kind: "sub_agent_finished", subAgentId: "task-1", output: "found" },
|
|
206
|
+
]);
|
|
207
|
+
const task = rowOf(status, "task-1");
|
|
208
|
+
const [sub] = status.subAgentExecutions;
|
|
209
|
+
const child = sub.messages.flatMap((m) => m.toolCalls)[0];
|
|
210
|
+
expect([task.startedAt, task.completedAt, sub.startedAt, sub.completedAt, child.startedAt, child.completedAt, sub.messages[0].timestamp]).toEqual(Array(7).fill(T0));
|
|
211
|
+
expect(new Date().toISOString(), "the clock did move between folds").not.toBe(T0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("a fact that carries its own observedAt keeps it inside a batch", () => {
|
|
215
|
+
const { sb, status } = ticking();
|
|
216
|
+
sb.applyObservation([
|
|
217
|
+
{ kind: "tool_started", callId: "c1", name: "read_file", input: {}, mcpServerSlug: "" },
|
|
218
|
+
{ kind: "tool_finished", callId: "c1", result: "", observedAt: "2025-12-31T23:59:58.000Z" },
|
|
219
|
+
]);
|
|
220
|
+
expect([rowOf(status, "c1").startedAt, rowOf(status, "c1").completedAt]).toEqual([T0, "2025-12-31T23:59:58.000Z"]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("a re-emitted start inside a batch leaves the row's first start alone", () => {
|
|
224
|
+
const { sb, status } = ticking();
|
|
225
|
+
sb.applyObservation([{ kind: "tool_started", callId: "c1", name: "read_file", input: {}, mcpServerSlug: "" }]);
|
|
226
|
+
vi.setSystemTime(new Date("2026-01-01T00:00:05.000Z"));
|
|
227
|
+
sb.applyObservation([
|
|
228
|
+
{ kind: "tool_started", callId: "c1", name: "read_file", input: {}, mcpServerSlug: "" },
|
|
229
|
+
{ kind: "tool_finished", callId: "c1", result: "ok" },
|
|
230
|
+
]);
|
|
231
|
+
expect([rowOf(status, "c1").startedAt, rowOf(status, "c1").completedAt]).toEqual([T0, "2026-01-01T00:00:05.000Z"]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("apply alone reads the clock per fact, and a batch ends with its call", () => {
|
|
235
|
+
const { sb, status } = ticking();
|
|
236
|
+
sb.apply({ kind: "tool_started", callId: "c1", name: "read_file", input: {}, mcpServerSlug: "" });
|
|
237
|
+
sb.apply({ kind: "tool_finished", callId: "c1", result: "ok" });
|
|
238
|
+
expect(rowOf(status, "c1").completedAt).toBe("2026-01-01T00:00:00.001Z");
|
|
239
|
+
sb.applyObservation([{ kind: "tool_started", callId: "c2", name: "read_file", input: {}, mcpServerSlug: "" }]);
|
|
240
|
+
sb.apply({ kind: "tool_finished", callId: "c2", result: "ok" });
|
|
241
|
+
expect([rowOf(status, "c2").startedAt, rowOf(status, "c2").completedAt]).toEqual(["2026-01-01T00:00:00.002Z", "2026-01-01T00:00:00.003Z"]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
177
245
|
describe("TranscriptBuilder — every row with args carries them redacted, and an elided, redacted argsPreview", () => {
|
|
178
246
|
it("an ungated row's preview is its args, secret keys redacted", () => {
|
|
179
247
|
const { status } = feed(builder(), [
|
|
@@ -327,17 +395,21 @@ describe("TranscriptBuilder — approval_proposed is the one place a row is ever
|
|
|
327
395
|
const { sb, status } = feed(builder(), [
|
|
328
396
|
{ kind: "message_start", runId: "run-1" },
|
|
329
397
|
{ kind: "text_delta", runId: "run-1", text: "Running the build." },
|
|
330
|
-
{ kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: ""
|
|
398
|
+
{ kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: "" },
|
|
331
399
|
{ kind: "tool_error", callId: "sh-1", message: "Blocked by hook" },
|
|
332
400
|
]);
|
|
333
|
-
const stamped = rowOf(status, "sh-1").approvalRequestedAt;
|
|
334
401
|
sb.apply(proposal("sh-1", { name: "Shell", message: "Run command: make", contentDigest: "d-1" }));
|
|
335
402
|
const row = rowOf(status, "sh-1");
|
|
336
403
|
expect(row.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
|
|
404
|
+
expect(row.requiresApproval, "the proposal is the one writer of the hold").toBe(true);
|
|
405
|
+
expect(row.approvalMessage).toBe("Run command: make");
|
|
337
406
|
expect(row.error).toBe("");
|
|
338
407
|
expect(row.result).toBe("");
|
|
339
408
|
expect(row.completedAt).toBe("");
|
|
340
|
-
|
|
409
|
+
const stamped = row.approvalRequestedAt;
|
|
410
|
+
expect(stamped).toContain("T");
|
|
411
|
+
sb.apply(proposal("sh-1", { name: "Shell", message: "Run command: make", contentDigest: "d-1" }));
|
|
412
|
+
expect(rowOf(status, "sh-1").approvalRequestedAt, "the first stamp stands").toBe(stamped);
|
|
341
413
|
expect(row.approvalContentDigest).toBe("d-1");
|
|
342
414
|
expect(status.messages.flatMap((m) => m.toolCalls), "no second row").toHaveLength(1);
|
|
343
415
|
expect(sb.awaitingApproval).toBe(true);
|
|
@@ -347,7 +419,7 @@ describe("TranscriptBuilder — approval_proposed is the one place a row is ever
|
|
|
347
419
|
const { sb, status } = feed(builder(), [
|
|
348
420
|
{ kind: "message_start", runId: "run-1" },
|
|
349
421
|
{ kind: "text_delta", runId: "run-1", text: "Editing notes." },
|
|
350
|
-
{ kind: "tool_started", callId: "ed-1", name: "edit", input: { path: "notes.md", content: "partial" }, mcpServerSlug: ""
|
|
422
|
+
{ kind: "tool_started", callId: "ed-1", name: "edit", input: { path: "notes.md", content: "partial" }, mcpServerSlug: "" },
|
|
351
423
|
{ kind: "tool_error", callId: "ed-1", message: "Blocked by hook" },
|
|
352
424
|
]);
|
|
353
425
|
// The boundary's overlay carries the hook's captured input — the whole
|
|
@@ -368,7 +440,7 @@ describe("TranscriptBuilder — approval_proposed is the one place a row is ever
|
|
|
368
440
|
|
|
369
441
|
it("no tool_started ever sets awaitingApproval; only a proposal does", () => {
|
|
370
442
|
const { sb, status } = feed(builder(), [
|
|
371
|
-
{ kind: "tool_started", callId: "sh-1", name: "Shell", input: {}, mcpServerSlug: "",
|
|
443
|
+
{ kind: "tool_started", callId: "sh-1", name: "Shell", input: {}, mcpServerSlug: "", provenance: "builtin_category" },
|
|
372
444
|
]);
|
|
373
445
|
expect(sb.awaitingApproval).toBe(false);
|
|
374
446
|
expect(rowOf(status, "sh-1").status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
|
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
* observed earlier and delivers later stamps its own instant
|
|
49
49
|
* (`observedAt`); an output chunk for a settled row is stale
|
|
50
50
|
* and dropped — the completion's result is the whole output.
|
|
51
|
+
* - One observation, one instant: the facts a translator made of one
|
|
52
|
+
* raw engine event, folded through {@link applyObservation}, share
|
|
53
|
+
* one clock read for every stamp they make (a Cursor completion with
|
|
54
|
+
* no running before it starts and finishes at the same instant,
|
|
55
|
+
* stigmer#1390); {@link apply} alone reads the clock per fact.
|
|
51
56
|
* - The AI-message boundary (native's own rule, made correct by
|
|
52
57
|
* scoping): a tool row attaches to the scope's current AI message — the
|
|
53
58
|
* latest with text, the seed's last AI message over a seeded transcript
|
|
@@ -148,6 +153,8 @@ export class TranscriptBuilder {
|
|
|
148
153
|
private readonly observer: TranscriptObserver | undefined;
|
|
149
154
|
private _dirty = false;
|
|
150
155
|
private _awaitingApproval = false;
|
|
156
|
+
/** The one instant of the observation being folded, while {@link applyObservation} runs; absent otherwise. */
|
|
157
|
+
private observationInstant: string | undefined;
|
|
151
158
|
|
|
152
159
|
/**
|
|
153
160
|
* Builds INTO `status`, by reference: its `messages`, `subAgentExecutions`
|
|
@@ -233,6 +240,32 @@ export class TranscriptBuilder {
|
|
|
233
240
|
}
|
|
234
241
|
}
|
|
235
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Fold the facts ONE harness observation yields — the events a translator
|
|
245
|
+
* made of one raw engine event — at one instant: the clock is read once,
|
|
246
|
+
* and every stamp the facts make (a row's start and finish, a sub-agent's
|
|
247
|
+
* open and close, a message's timestamp) is that instant. A Cursor
|
|
248
|
+
* completion the stream reports with no running before it is a start and a
|
|
249
|
+
* finish in one event, and it must carry one instant, not two reads a
|
|
250
|
+
* millisecond apart (stigmer#1390). A fact that carries its own
|
|
251
|
+
* `observedAt` keeps it. Each fact is applied exactly as {@link apply}
|
|
252
|
+
* applies it, observer included.
|
|
253
|
+
*/
|
|
254
|
+
applyObservation(facts: readonly TranscriptEvent[]): void {
|
|
255
|
+
const outer = this.observationInstant;
|
|
256
|
+
this.observationInstant = outer ?? utcTimestamp();
|
|
257
|
+
try {
|
|
258
|
+
for (const fact of facts) this.apply(fact);
|
|
259
|
+
} finally {
|
|
260
|
+
this.observationInstant = outer;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The instant a stamp records: the observation's, while one is being folded, else now. */
|
|
265
|
+
private instant(): string {
|
|
266
|
+
return this.observationInstant ?? utcTimestamp();
|
|
267
|
+
}
|
|
268
|
+
|
|
236
269
|
private fold(event: TranscriptEvent): void {
|
|
237
270
|
try {
|
|
238
271
|
switch (event.kind) {
|
|
@@ -354,7 +387,7 @@ export class TranscriptBuilder {
|
|
|
354
387
|
subject: event.subject,
|
|
355
388
|
input: event.input,
|
|
356
389
|
status: SubAgentStatus.SUB_AGENT_IN_PROGRESS,
|
|
357
|
-
startedAt:
|
|
390
|
+
startedAt: this.instant(),
|
|
358
391
|
});
|
|
359
392
|
this.state.openSubAgent(row);
|
|
360
393
|
this._dirty = true;
|
|
@@ -364,7 +397,7 @@ export class TranscriptBuilder {
|
|
|
364
397
|
const scope = this.state.subAgent(event.subAgentId);
|
|
365
398
|
if (!scope) return;
|
|
366
399
|
scope.row.status = SubAgentStatus.SUB_AGENT_COMPLETED;
|
|
367
|
-
scope.row.completedAt =
|
|
400
|
+
scope.row.completedAt = this.instant();
|
|
368
401
|
if (event.output !== undefined) scope.row.output = event.output;
|
|
369
402
|
finalizeStreaming(scope.transcript);
|
|
370
403
|
this._dirty = true;
|
|
@@ -374,7 +407,7 @@ export class TranscriptBuilder {
|
|
|
374
407
|
const scope = this.state.subAgent(event.subAgentId);
|
|
375
408
|
if (!scope) return;
|
|
376
409
|
scope.row.status = SubAgentStatus.SUB_AGENT_FAILED;
|
|
377
|
-
scope.row.completedAt =
|
|
410
|
+
scope.row.completedAt = this.instant();
|
|
378
411
|
scope.row.error = event.error;
|
|
379
412
|
finalizeStreaming(scope.transcript);
|
|
380
413
|
this._dirty = true;
|
|
@@ -427,7 +460,7 @@ export class TranscriptBuilder {
|
|
|
427
460
|
const msg = create(AgentMessageSchema, {
|
|
428
461
|
type: MessageType.MESSAGE_THINKING,
|
|
429
462
|
content: text,
|
|
430
|
-
timestamp:
|
|
463
|
+
timestamp: this.instant(),
|
|
431
464
|
isStreaming: true,
|
|
432
465
|
});
|
|
433
466
|
scope.messages.push(msg);
|
|
@@ -466,17 +499,16 @@ export class TranscriptBuilder {
|
|
|
466
499
|
// only when the scope has no message yet.
|
|
467
500
|
const parentMsg = scope.currentAiMessage ?? this.ensureAiMessageForToolCall(scope);
|
|
468
501
|
|
|
469
|
-
// A started call is RUNNING
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
// about approval.
|
|
502
|
+
// A started call is RUNNING and carries no approval fact: whether it is
|
|
503
|
+
// held is known only after the engine's gate has run, and a held call
|
|
504
|
+
// reaches the transcript as `approval_proposed` (Cursor's boundary after
|
|
505
|
+
// the hook denied it; native's post-stream seed, where a held call never
|
|
506
|
+
// starts at all). The builder decides nothing about approval.
|
|
475
507
|
const tc = create(ToolCallSchema, {
|
|
476
508
|
id: callId,
|
|
477
509
|
name,
|
|
478
510
|
status: ToolCallStatus.TOOL_CALL_RUNNING,
|
|
479
|
-
startedAt:
|
|
511
|
+
startedAt: this.instant(),
|
|
480
512
|
});
|
|
481
513
|
|
|
482
514
|
if (Object.keys(input).length > 0) {
|
|
@@ -503,13 +535,6 @@ export class TranscriptBuilder {
|
|
|
503
535
|
tc.policyEngineVersion = POLICY_ENGINE_VERSION;
|
|
504
536
|
}
|
|
505
537
|
|
|
506
|
-
// The gate's word: the row says it is gated and what the card will ask;
|
|
507
|
-
// `approvalRequestedAt` is stamped when the boundary actually parks it
|
|
508
|
-
// (`approval_proposed`, or a gated row's `tool_error`), never here.
|
|
509
|
-
if (event.gate) {
|
|
510
|
-
tc.requiresApproval = true;
|
|
511
|
-
tc.approvalMessage = event.gate.message;
|
|
512
|
-
}
|
|
513
538
|
|
|
514
539
|
parentMsg.toolCalls.push(tc);
|
|
515
540
|
scope.toolCalls.set(callId, tc);
|
|
@@ -543,8 +568,8 @@ export class TranscriptBuilder {
|
|
|
543
568
|
const resultChanged = !!result && result !== tc.result;
|
|
544
569
|
if (!wasSettled) tc.status = ToolCallStatus.TOOL_CALL_COMPLETED;
|
|
545
570
|
// The instant the harness OBSERVED the completion when it delivers it
|
|
546
|
-
// later (Cursor's queued delta); otherwise
|
|
547
|
-
if (!tc.completedAt) tc.completedAt = observedAt ??
|
|
571
|
+
// later (Cursor's queued delta); otherwise the fold's instant.
|
|
572
|
+
if (!tc.completedAt) tc.completedAt = observedAt ?? this.instant();
|
|
548
573
|
if (resultChanged) tc.result = result;
|
|
549
574
|
stopStreaming(tc);
|
|
550
575
|
scope.argBuffers.delete(callId);
|
|
@@ -571,17 +596,15 @@ export class TranscriptBuilder {
|
|
|
571
596
|
const tc = scope.toolCalls.get(callId);
|
|
572
597
|
if (!tc) return;
|
|
573
598
|
|
|
574
|
-
// The same upsert as a finish. A
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
// moment approval became due is stamped here if nothing stamped it yet.
|
|
599
|
+
// The same upsert as a finish. A call the Cursor hook denied also reports
|
|
600
|
+
// `error` here; it is an ordinary failure until the boundary parks it
|
|
601
|
+
// through `approval_proposed`, which stamps the approval fields.
|
|
578
602
|
const wasSettled = isSettled(tc.status);
|
|
579
603
|
const messageChanged = !!message && message !== tc.error;
|
|
580
|
-
const at = observedAt ??
|
|
604
|
+
const at = observedAt ?? this.instant();
|
|
581
605
|
if (!wasSettled) tc.status = ToolCallStatus.TOOL_CALL_FAILED;
|
|
582
606
|
if (!tc.completedAt) tc.completedAt = at;
|
|
583
607
|
if (messageChanged) tc.error = message;
|
|
584
|
-
if (tc.requiresApproval && !tc.approvalRequestedAt) tc.approvalRequestedAt = at;
|
|
585
608
|
stopStreaming(tc);
|
|
586
609
|
scope.argBuffers.delete(callId);
|
|
587
610
|
|
|
@@ -629,7 +652,7 @@ export class TranscriptBuilder {
|
|
|
629
652
|
// ── Post-Stream Facts ──────────────────────────────────────────────
|
|
630
653
|
|
|
631
654
|
private handleApprovalProposed(scope: Transcript, event: ApprovalProposedEvent): void {
|
|
632
|
-
const now =
|
|
655
|
+
const now = this.instant();
|
|
633
656
|
const existing = scope.toolCalls.get(event.callId);
|
|
634
657
|
if (existing) {
|
|
635
658
|
// Reopen (Cursor's `markWaitingApproval`): the boundary is re-proposing
|
|
@@ -693,7 +716,7 @@ export class TranscriptBuilder {
|
|
|
693
716
|
scope.messages.push(create(AgentMessageSchema, {
|
|
694
717
|
type: MessageType.MESSAGE_SYSTEM,
|
|
695
718
|
content: text,
|
|
696
|
-
timestamp:
|
|
719
|
+
timestamp: this.instant(),
|
|
697
720
|
}));
|
|
698
721
|
this._dirty = true;
|
|
699
722
|
}
|
|
@@ -712,7 +735,7 @@ export class TranscriptBuilder {
|
|
|
712
735
|
const msg = create(AgentMessageSchema, {
|
|
713
736
|
type: MessageType.MESSAGE_AI,
|
|
714
737
|
content: "",
|
|
715
|
-
timestamp:
|
|
738
|
+
timestamp: this.instant(),
|
|
716
739
|
isStreaming: true,
|
|
717
740
|
});
|
|
718
741
|
|
|
@@ -737,7 +760,7 @@ export class TranscriptBuilder {
|
|
|
737
760
|
const msg = create(AgentMessageSchema, {
|
|
738
761
|
type: MessageType.MESSAGE_AI,
|
|
739
762
|
content: "",
|
|
740
|
-
timestamp:
|
|
763
|
+
timestamp: this.instant(),
|
|
741
764
|
isStreaming: false,
|
|
742
765
|
});
|
|
743
766
|
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
* what a row or a message carries; usage is neither.
|
|
19
19
|
* - `tool_finished` carries `result: string`; the engine's output
|
|
20
20
|
* envelope is the translator's to render.
|
|
21
|
-
* - `tool_started` carries the attribution (`mcpServerSlug`)
|
|
22
|
-
* provenance
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* - `tool_started` carries the attribution (`mcpServerSlug`) and the
|
|
22
|
+
* provenance, which the translator resolves from the harness's policy
|
|
23
|
+
* state. It never says whether the call is held: a started call is
|
|
24
|
+
* running, and a held one reaches the transcript as `approval_proposed`
|
|
25
|
+
* on both harnesses, the one writer of a row's approval fields (#1117).
|
|
26
|
+
* The builder writes what it is told and decides nothing about approval.
|
|
26
27
|
* - Scope is `subAgentId?` — the only scope either harness has is
|
|
27
28
|
* "the root, or one sub-agent by the id its row carries" — and the
|
|
28
29
|
* translator says when a sub-agent opens and closes
|
|
@@ -89,25 +90,18 @@ export interface MessageFinishEvent extends Scoped {
|
|
|
89
90
|
// ── Tool Events ───────────────────────────────────────────────────
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* has
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* row is never WAITING at creation on either harness; the plan's `parked:
|
|
104
|
-
* true` arm had no producer and was dropped.
|
|
93
|
+
* A tool call has started. It carries no approval fact, on either harness
|
|
94
|
+
* (#1117): whether a call is HELD for a person's decision is known only after
|
|
95
|
+
* the engine's own gate has run, and it reaches the transcript as
|
|
96
|
+
* `approval_proposed` — native's post-stream seed after LangGraph's
|
|
97
|
+
* `interrupt()` (which fires before the tool handler, so a held call never
|
|
98
|
+
* starts), Cursor's deny-and-retry boundary after the preToolUse hook denied
|
|
99
|
+
* it. A call that runs under a lease, a bypass or capture mode is therefore
|
|
100
|
+
* never marked as requiring approval. A `gate` member carried the policy's
|
|
101
|
+
* category verdict here until #1117; it had one producer (Cursor), which made
|
|
102
|
+
* the field mean different things per harness, and it was dropped, as a
|
|
103
|
+
* `parked: true` arm with no producer was before it.
|
|
105
104
|
*/
|
|
106
|
-
export interface GateAnswer {
|
|
107
|
-
/** The approval card's message, placeholders already resolved. */
|
|
108
|
-
readonly message: string;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
105
|
export interface ToolStartedEvent extends Scoped {
|
|
112
106
|
readonly kind: "tool_started";
|
|
113
107
|
readonly callId: string;
|
|
@@ -121,7 +115,6 @@ export interface ToolStartedEvent extends Scoped {
|
|
|
121
115
|
* on both harnesses today, #1133) or no layer governs it.
|
|
122
116
|
*/
|
|
123
117
|
readonly provenance?: PolicySource;
|
|
124
|
-
readonly gate?: GateAnswer;
|
|
125
118
|
}
|
|
126
119
|
|
|
127
120
|
export interface ToolArgDeltaEvent extends Scoped {
|
|
@@ -139,15 +132,17 @@ export interface ToolOutputDeltaEvent extends Scoped {
|
|
|
139
132
|
/**
|
|
140
133
|
* A fact the harness observed at one instant and delivers at another. The
|
|
141
134
|
* builder is the transcript's one clock — every row stamp is `utcTimestamp()`
|
|
142
|
-
* at apply
|
|
135
|
+
* at apply, read once per observation when the loop folds a raw event's
|
|
136
|
+
* facts together (`TranscriptBuilder.applyObservation`) — except here: a
|
|
137
|
+
* Cursor completion arrives first on the SDK's
|
|
143
138
|
* delta channel, and the translator must QUEUE it for the loop to apply after
|
|
144
139
|
* the current stream event (a delta applied mid-persist would land on a row
|
|
145
140
|
* the offload is replacing). Without the observed
|
|
146
141
|
* instant the row's `completedAt` would be the fold's moment, one stream
|
|
147
142
|
* event late — seconds late while the model narrates — and the tool's
|
|
148
143
|
* duration in the console would lie (fixed 2026-09-15, #1097). Absent
|
|
149
|
-
* means
|
|
150
|
-
* never set it.
|
|
144
|
+
* means the fold's instant: native, and every fact a translator emits as it
|
|
145
|
+
* sees it, never set it.
|
|
151
146
|
*/
|
|
152
147
|
interface Observed {
|
|
153
148
|
readonly observedAt?: string;
|
|
@@ -649,7 +649,9 @@ export async function reconcileReinvocation(
|
|
|
649
649
|
* The MCP-bound env map (and ONLY it, never the agent process env) carries
|
|
650
650
|
* the reserved caller-identity keys, so a server that declares them in
|
|
651
651
|
* spec.env can template the platform-verified caller into its headers;
|
|
652
|
-
* filterEnvToDeclaredKeys keeps every other server blind.
|
|
652
|
+
* filterEnvToDeclaredKeys keeps every other server blind. Resolution fills a
|
|
653
|
+
* missing STIGMER_SERVER_ADDRESS per server from `config`'s endpoints
|
|
654
|
+
* (shared/platform-server-address.ts). The resolved list
|
|
653
655
|
* then mutates through the Connect backfill and three synthesized
|
|
654
656
|
* attachments, deliberately AFTER resolve + backfill so the backfill's
|
|
655
657
|
* destructiveHint tightener can never force-gate an attachment's tools:
|
|
@@ -702,7 +704,7 @@ export async function resolveMcpServersAndPolicies(
|
|
|
702
704
|
sessionId,
|
|
703
705
|
);
|
|
704
706
|
let servers = (await resolveMcpServers(
|
|
705
|
-
client, blueprint.mergedMcpServerUsages, mcpEnvVars, transportPosture,
|
|
707
|
+
client, blueprint.mergedMcpServerUsages, mcpEnvVars, transportPosture, config,
|
|
706
708
|
)).resolvedServers;
|
|
707
709
|
deps.timing.mark("resolve_mcp_servers");
|
|
708
710
|
|
|
@@ -710,7 +712,7 @@ export async function resolveMcpServersAndPolicies(
|
|
|
710
712
|
const sessionOrg = session.metadata?.org ?? "";
|
|
711
713
|
servers = await backfillMcpServersIfNeeded(
|
|
712
714
|
client, servers, blueprint.mergedMcpServerUsages, mcpEnvVars, sessionOrg,
|
|
713
|
-
transportPosture, deps.heartbeat, environment.secretKeys,
|
|
715
|
+
transportPosture, config, deps.heartbeat, environment.secretKeys,
|
|
714
716
|
);
|
|
715
717
|
deps.timing.mark("backfill_mcp");
|
|
716
718
|
|
package/src/main.ts
CHANGED
|
@@ -46,6 +46,7 @@ import type { IpcCommand, IpcResponse } from "./ipc-protocol.js";
|
|
|
46
46
|
|
|
47
47
|
import { handleUnhandledRejection } from "./activities/execute-cursor/rejection-capture.js";
|
|
48
48
|
import { installProcessPipeGuards, reportFatal } from "./pipe-safety.js";
|
|
49
|
+
import { armExitBackstop } from "./exit-backstop.js";
|
|
49
50
|
|
|
50
51
|
// Guard the host pipes before anything writes to them. A dropped stderr/stdout
|
|
51
52
|
// reader otherwise turns the next write into an uncaught EPIPE loop that starves
|
|
@@ -510,17 +511,20 @@ async function main(): Promise<void> {
|
|
|
510
511
|
await runManagerMode(config);
|
|
511
512
|
// Manager mode is driven by the host over stdin. Once it returns — via the IPC `shutdown`
|
|
512
513
|
// command or stdin EOF when the host process dies — exit deterministically so a stray open
|
|
513
|
-
// handle can't keep a shut-down runner alive as an orphan (issue #177).
|
|
514
|
-
// to exit naturally so its OTel flush completes.
|
|
514
|
+
// handle can't keep a shut-down runner alive as an orphan (issue #177).
|
|
515
515
|
process.exit(0);
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
// Static and pool modes exit naturally once their shutdown has returned (worker drained,
|
|
519
|
+
// OTel flushed). The backstop is the safety net, not the path: a healthy runner exits in
|
|
520
|
+
// milliseconds and it never fires; a leaked handle gets a named bound and a log line naming
|
|
521
|
+
// it, instead of holding the process for the orchestrator's whole grace (exit-backstop.ts).
|
|
518
522
|
if (runnerMode === "pool") {
|
|
519
523
|
await runPoolMode(config, poolMemberId!);
|
|
520
|
-
|
|
524
|
+
} else {
|
|
525
|
+
await runStaticMode(config);
|
|
521
526
|
}
|
|
522
|
-
|
|
523
|
-
await runStaticMode(config);
|
|
527
|
+
armExitBackstop();
|
|
524
528
|
}
|
|
525
529
|
|
|
526
530
|
main().catch((err) => {
|