@stigmer/runner 3.10.0 → 3.11.1-dev.20260812192248
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 +14 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-llm.js +9 -10
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +2 -1
- package/dist/activities/classify-tool-approvals.js +28 -2
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +39 -0
- package/dist/activities/discover-mcp-server.js +165 -28
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/emit-event.d.ts +14 -2
- package/dist/activities/emit-event.js +52 -17
- package/dist/activities/emit-event.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
- package/dist/activities/execute-cursor/approval-state.js +7 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +18 -4
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/env-resolver.js +3 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +59 -3
- package/dist/activities/execute-cursor/error-classifier.js +115 -6
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
- package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
- package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
- package/dist/activities/execute-cursor/hook-script.js +72 -10
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +51 -11
- package/dist/activities/execute-cursor/index.js +177 -93
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
- package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
- package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
- package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
- package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +51 -4
- package/dist/activities/execute-cursor/prompt-builder.js +49 -7
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
- package/dist/activities/execute-cursor/turn-recovery.js +193 -0
- package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
- package/dist/activities/execute-cursor/turn-stream.js +4 -1
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/environment.js +3 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +15 -0
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
- package/dist/activities/execute-deep-agent/setup.js +65 -24
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
- package/dist/activities/execute-deep-agent/shell-env.js +7 -9
- package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +15 -2
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +27 -6
- package/dist/activities/execute-deep-agent/subagent-wiring.js +26 -5
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +8 -3
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/run-command.d.ts +5 -2
- package/dist/activities/run-command.js +22 -10
- package/dist/activities/run-command.js.map +1 -1
- package/dist/activities/run-env.d.ts +40 -0
- package/dist/activities/run-env.js +66 -0
- package/dist/activities/run-env.js.map +1 -0
- package/dist/activities/workflow-event-activities.d.ts +28 -10
- package/dist/activities/workflow-event-activities.js +87 -58
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/bootstrap.js +5 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +21 -1
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +83 -21
- package/dist/client/stigmer-client.js +106 -29
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +64 -0
- package/dist/encryption/config.js +108 -0
- package/dist/encryption/config.js.map +1 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.js +3 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/payload-codec.d.ts +41 -0
- package/dist/encryption/payload-codec.js +130 -0
- package/dist/encryption/payload-codec.js.map +1 -0
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +7 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/path-normalization.d.ts +57 -0
- package/dist/middleware/path-normalization.js +109 -0
- package/dist/middleware/path-normalization.js.map +1 -0
- package/dist/middleware/types.d.ts +8 -0
- package/dist/payload-codecs.d.ts +17 -0
- package/dist/payload-codecs.js +42 -0
- package/dist/payload-codecs.js.map +1 -0
- package/dist/preflight.d.ts +31 -0
- package/dist/preflight.js +43 -0
- package/dist/preflight.js.map +1 -1
- package/dist/runner-manager.js +21 -17
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +11 -18
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +9 -3
- package/dist/shared/approval-policy.js +15 -6
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +23 -1
- package/dist/shared/artifact-storage.js +51 -10
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/attachment-naming.d.ts +53 -0
- package/dist/shared/attachment-naming.js +59 -0
- package/dist/shared/attachment-naming.js.map +1 -0
- package/dist/shared/caller-identity.d.ts +23 -2
- package/dist/shared/caller-identity.js +36 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +1 -0
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +26 -1
- package/dist/shared/checkpointer/http-saver.js +54 -8
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-attachment.js +1 -0
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/conversation-catchup.d.ts +9 -2
- package/dist/shared/conversation-catchup.js +39 -6
- package/dist/shared/conversation-catchup.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +50 -7
- package/dist/shared/datastore-attachment.js +93 -11
- package/dist/shared/datastore-attachment.js.map +1 -1
- package/dist/shared/grpc-retry.d.ts +5 -1
- package/dist/shared/grpc-retry.js +5 -1
- package/dist/shared/grpc-retry.js.map +1 -1
- package/dist/shared/http-retry.d.ts +85 -0
- package/dist/shared/http-retry.js +101 -0
- package/dist/shared/http-retry.js.map +1 -0
- package/dist/shared/llm-backend.d.ts +275 -0
- package/dist/shared/llm-backend.js +425 -0
- package/dist/shared/llm-backend.js.map +1 -0
- package/dist/shared/llm-proxy.d.ts +8 -0
- package/dist/shared/llm-proxy.js +15 -0
- package/dist/shared/llm-proxy.js.map +1 -1
- package/dist/shared/mcp-enabled-tools.d.ts +57 -0
- package/dist/shared/mcp-enabled-tools.js +86 -0
- package/dist/shared/mcp-enabled-tools.js.map +1 -0
- package/dist/shared/mcp-manager.d.ts +11 -5
- package/dist/shared/mcp-manager.js +47 -8
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +39 -2
- package/dist/shared/mcp-resolver.js +38 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
- package/dist/shared/mcp-schema-sanitizer.js +197 -0
- package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
- package/dist/shared/model-client.d.ts +18 -5
- package/dist/shared/model-client.js +149 -18
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-error.js +198 -5
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/model-pricing-data.d.ts +2 -0
- package/dist/shared/model-pricing-data.js +13 -3
- package/dist/shared/model-pricing-data.js.map +1 -1
- package/dist/shared/model-registry.js +3 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +33 -0
- package/dist/shared/plan-mode-permissions.js +35 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -0
- package/dist/shared/registry-endpoint.d.ts +20 -0
- package/dist/shared/registry-endpoint.js +25 -0
- package/dist/shared/registry-endpoint.js.map +1 -1
- package/dist/shared/runner-credential-keys.d.ts +24 -0
- package/dist/shared/runner-credential-keys.js +47 -0
- package/dist/shared/runner-credential-keys.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +2 -4
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/resolve.d.ts +19 -1
- package/dist/workflow-engine/resolve.js +37 -2
- package/dist/workflow-engine/resolve.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +18 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
- package/dist/workflows/call-agent-orchestrator.js +1 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +9 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/engine-core.js +23 -2
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +1 -1
- package/dist/workflows/execute-from-execution.js +11 -1
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +9 -4
- package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
- package/src/__test-utils__/vitest-global-setup.ts +50 -0
- package/src/__tests__/bootstrap.test.ts +43 -0
- package/src/__tests__/claimcheck-codec.test.ts +36 -0
- package/src/__tests__/encryption-codec.test.ts +287 -0
- package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
- package/src/__tests__/history-encryption-e2e.test.ts +243 -0
- package/src/__tests__/preflight.test.ts +65 -2
- package/src/activities/__tests__/call-llm.test.ts +75 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +242 -1
- package/src/activities/__tests__/error-classifier.test.ts +94 -2
- package/src/activities/__tests__/run-command.test.ts +221 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
- package/src/activities/call-llm.ts +9 -16
- package/src/activities/classify-tool-approvals.ts +34 -4
- package/src/activities/discover-mcp-server.ts +203 -31
- package/src/activities/emit-event.ts +65 -21
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +53 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +151 -16
- package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
- package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
- package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
- package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
- package/src/activities/execute-cursor/approval-state.ts +30 -1
- package/src/activities/execute-cursor/attachment-resolver.ts +31 -3
- package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
- package/src/activities/execute-cursor/env-resolver.ts +3 -1
- package/src/activities/execute-cursor/error-classifier.ts +131 -7
- package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
- package/src/activities/execute-cursor/hook-script.ts +74 -10
- package/src/activities/execute-cursor/index.ts +209 -109
- package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
- package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
- package/src/activities/execute-cursor/prompt-builder.ts +85 -9
- package/src/activities/execute-cursor/turn-recovery.ts +208 -0
- package/src/activities/execute-cursor/turn-stream.ts +5 -2
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +110 -8
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +178 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
- package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
- package/src/activities/execute-deep-agent/environment.ts +3 -1
- package/src/activities/execute-deep-agent/index.ts +20 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
- package/src/activities/execute-deep-agent/setup.ts +85 -29
- package/src/activities/execute-deep-agent/shell-env.ts +8 -9
- package/src/activities/execute-deep-agent/subagent-transformer.ts +30 -2
- package/src/activities/execute-deep-agent/subagent-wiring.ts +39 -6
- package/src/activities/hydrate-workflow-execution.ts +8 -3
- package/src/activities/run-command.ts +25 -12
- package/src/activities/run-env.ts +79 -0
- package/src/activities/workflow-event-activities.ts +96 -69
- package/src/bootstrap.ts +5 -0
- package/src/claimcheck/payload-codec.ts +33 -1
- package/src/client/__tests__/stigmer-client.test.ts +110 -11
- package/src/client/stigmer-client.ts +150 -37
- package/src/encryption/config.ts +155 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/payload-codec.ts +152 -0
- package/src/middleware/__tests__/path-normalization.test.ts +140 -0
- package/src/middleware/index.ts +8 -0
- package/src/middleware/path-normalization.ts +125 -0
- package/src/middleware/types.ts +11 -0
- package/src/payload-codecs.ts +62 -0
- package/src/preflight.ts +45 -0
- package/src/runner-manager.ts +26 -26
- package/src/runner.ts +12 -27
- package/src/shared/__tests__/approval-policy.test.ts +82 -39
- package/src/shared/__tests__/artifact-storage.test.ts +145 -2
- package/src/shared/__tests__/attachment-naming.test.ts +159 -0
- package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
- package/src/shared/__tests__/caller-identity.test.ts +25 -0
- package/src/shared/__tests__/channel-attachment.test.ts +1 -1
- package/src/shared/__tests__/connect-backfill.test.ts +1 -0
- package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
- package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
- package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
- package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
- package/src/shared/__tests__/foundry-seam.test.ts +482 -0
- package/src/shared/__tests__/http-retry.test.ts +68 -0
- package/src/shared/__tests__/llm-backend.test.ts +616 -0
- package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
- package/src/shared/__tests__/mcp-manager.test.ts +137 -15
- package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
- package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
- package/src/shared/__tests__/model-client.test.ts +200 -0
- package/src/shared/__tests__/model-error.test.ts +289 -1
- package/src/shared/__tests__/model-pricing.test.ts +62 -2
- package/src/shared/__tests__/model-registry.test.ts +22 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
- package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
- package/src/shared/__tests__/vertex-seam.test.ts +295 -0
- package/src/shared/approval-policy.ts +14 -7
- package/src/shared/artifact-storage.ts +75 -10
- package/src/shared/attachment-naming.ts +78 -0
- package/src/shared/caller-identity.ts +40 -5
- package/src/shared/channel-attachment.ts +1 -0
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
- package/src/shared/checkpointer/http-saver.ts +71 -8
- package/src/shared/conversation-attachment.ts +1 -0
- package/src/shared/conversation-catchup.ts +39 -6
- package/src/shared/datastore-attachment.ts +106 -11
- package/src/shared/grpc-retry.ts +5 -1
- package/src/shared/http-retry.ts +139 -0
- package/src/shared/llm-backend.ts +544 -0
- package/src/shared/llm-proxy.ts +15 -0
- package/src/shared/mcp-enabled-tools.ts +105 -0
- package/src/shared/mcp-manager.ts +56 -8
- package/src/shared/mcp-resolver.ts +73 -2
- package/src/shared/mcp-schema-sanitizer.ts +224 -0
- package/src/shared/model-client.ts +179 -19
- package/src/shared/model-error.ts +222 -4
- package/src/shared/model-pricing-data.ts +22 -3
- package/src/shared/model-registry.ts +7 -2
- package/src/shared/plan-mode-permissions.ts +37 -0
- package/src/shared/registry-endpoint.ts +27 -0
- package/src/shared/runner-credential-keys.ts +46 -0
- package/src/worker.ts +4 -5
- package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
- package/src/workflow-engine/resolve.ts +48 -2
- package/src/workflow-engine/types.ts +18 -0
- package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
- package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
- package/src/workflows/call-agent-orchestrator.ts +10 -0
- package/src/workflows/connect-mcp-server.ts +9 -0
- package/src/workflows/engine-core.ts +23 -2
- package/src/workflows/execute-from-execution.ts +12 -2
- package/src/workflows/types.ts +8 -0
- package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
- package/dist/activities/execute-cursor/mcp-config.js +0 -39
- package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
- package/src/activities/execute-cursor/mcp-config.ts +0 -66
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Naming policy for execution attachments — the single owner of the rule that
|
|
3
|
+
* resolves duplicate attachment filenames (issue #364).
|
|
4
|
+
*
|
|
5
|
+
* Both harnesses materialize attachments into the platform inputs namespace
|
|
6
|
+
* keyed by filename, so two attachments carrying the same name contend for
|
|
7
|
+
* one on-disk path. Before this module existed each harness had an accidental
|
|
8
|
+
* answer: the deep-agent injector failed the whole execution on the collision
|
|
9
|
+
* and the Cursor resolver silently overwrote the earlier file. The platform
|
|
10
|
+
* answer is neither — a duplicate is mechanically renamed (`report.pdf`,
|
|
11
|
+
* `report-2.pdf`, ...) and the rename is disclosed to the agent through the
|
|
12
|
+
* input-files prompt section, so no execution is ever burned over a
|
|
13
|
+
* resolvable name and no user file silently vanishes.
|
|
14
|
+
*
|
|
15
|
+
* The `stem-2.ext` semantics deliberately mirror the React SDK composer's
|
|
16
|
+
* client-side rename (sdk/react/src/attachment/attachment-utils.ts,
|
|
17
|
+
* `uniquifyFilename` — kept in sync by hand, the packages share no
|
|
18
|
+
* dependency), so a user sees the same rename shape whether the client or the
|
|
19
|
+
* runner performed it.
|
|
20
|
+
*
|
|
21
|
+
* Scope: this module owns NAMES only. What constitutes a collision is the
|
|
22
|
+
* caller's business — the deep-agent injector keys on full mount paths
|
|
23
|
+
* (explicit `mountPath` values participate), the Cursor resolver keys on bare
|
|
24
|
+
* filenames under `inputs/`. Callers that consider a collision a user
|
|
25
|
+
* contradiction (two attachments explicitly pinning the same mount path)
|
|
26
|
+
* keep rejecting; only mechanically derived names are renamed.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** The outcome of allocating a unique name for one attachment. */
|
|
30
|
+
export interface AllocatedName {
|
|
31
|
+
/** The final name — unchanged when it was free, `stem-N.ext` otherwise. */
|
|
32
|
+
readonly name: string;
|
|
33
|
+
/**
|
|
34
|
+
* The original requested name, present only when a rename happened — the
|
|
35
|
+
* disclosure payload the prompt builders render so the agent can still
|
|
36
|
+
* connect "the two report.pdfs" in the user's message to distinct files.
|
|
37
|
+
*/
|
|
38
|
+
readonly renamedFrom?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns `name` unchanged when it is not in `taken`, otherwise the first
|
|
43
|
+
* free `stem-2.ext`, `stem-3.ext`, … variant.
|
|
44
|
+
*
|
|
45
|
+
* Byte-for-byte twin of the React SDK's `uniquifyFilename`
|
|
46
|
+
* (sdk/react/src/attachment/attachment-utils.ts) so client-side and
|
|
47
|
+
* runner-side renames are indistinguishable to the user.
|
|
48
|
+
*/
|
|
49
|
+
export function uniquifyFilename(
|
|
50
|
+
name: string,
|
|
51
|
+
taken: ReadonlySet<string>,
|
|
52
|
+
): string {
|
|
53
|
+
if (!taken.has(name)) return name;
|
|
54
|
+
|
|
55
|
+
const dotIndex = name.lastIndexOf(".");
|
|
56
|
+
// A leading dot (".env") is a hidden-file prefix, not an extension.
|
|
57
|
+
const stem = dotIndex > 0 ? name.slice(0, dotIndex) : name;
|
|
58
|
+
const ext = dotIndex > 0 ? name.slice(dotIndex) : "";
|
|
59
|
+
|
|
60
|
+
for (let n = 2; ; n++) {
|
|
61
|
+
const candidate = `${stem}-${n}${ext}`;
|
|
62
|
+
if (!taken.has(candidate)) return candidate;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Allocate a unique name against `taken`, claiming the result in the set so
|
|
68
|
+
* sequential allocations see each other. Returns the disclosure payload
|
|
69
|
+
* (`renamedFrom`) when the name had to change.
|
|
70
|
+
*/
|
|
71
|
+
export function allocateUniqueName(
|
|
72
|
+
name: string,
|
|
73
|
+
taken: Set<string>,
|
|
74
|
+
): AllocatedName {
|
|
75
|
+
const unique = uniquifyFilename(name, taken);
|
|
76
|
+
taken.add(unique);
|
|
77
|
+
return unique === name ? { name: unique } : { name: unique, renamedFrom: name };
|
|
78
|
+
}
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* 2. The session creator (`stigmer_user`) from the Session resource's
|
|
11
11
|
* audit actor — console/CLI sessions have no channel sender, but the
|
|
12
12
|
* platform knows exactly who created the session.
|
|
13
|
-
* 3. The anonymous sentinel — discovery (no session exists)
|
|
14
|
-
* with no readable creator
|
|
13
|
+
* 3. The anonymous sentinel — discovery (no session exists), sessions
|
|
14
|
+
* with no readable creator, and sessions whose creator is the
|
|
15
|
+
* backend's "system" audit placeholder (not a principal — see
|
|
16
|
+
* SYSTEM_CREATOR_SENTINEL). Consumers must treat anonymous as a
|
|
15
17
|
* first-class caller: answer tools/list, refuse tool calls.
|
|
16
18
|
*
|
|
17
19
|
* Injection is opt-in by construction: the values enter the env map used
|
|
@@ -54,6 +56,20 @@ export const STIGMER_USER_KIND = "stigmer_user";
|
|
|
54
56
|
*/
|
|
55
57
|
export const ANONYMOUS_KIND = "anonymous";
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Audit-actor id that backends stamp when NO caller identity exists —
|
|
61
|
+
* the OSS server writes it on every create (no local auth), and the
|
|
62
|
+
* cloud's AuditActorBuilder falls back to it for caller-less internal
|
|
63
|
+
* writes. It names "nobody in particular": unrelated sessions from
|
|
64
|
+
* unrelated people all carry it, so presenting it as a caller identity
|
|
65
|
+
* would make the one string a grantable value that silently covers ALL
|
|
66
|
+
* such traffic in an MCP server's binding sheet. A creator matching this
|
|
67
|
+
* sentinel (and carrying no email) is therefore unresolvable and falls
|
|
68
|
+
* to anonymous — the deny-by-default the docs guide already promises
|
|
69
|
+
* for self-hosted backends.
|
|
70
|
+
*/
|
|
71
|
+
export const SYSTEM_CREATOR_SENTINEL = "system";
|
|
72
|
+
|
|
57
73
|
/** The resolved caller identity. */
|
|
58
74
|
export interface CallerIdentity {
|
|
59
75
|
kind: string;
|
|
@@ -79,6 +95,12 @@ export function anonymousCallerIdentity(): CallerIdentity {
|
|
|
79
95
|
* humans, and the audit actor's `id` field is historically mixed
|
|
80
96
|
* (identity-account id vs email — see the proto's own @internal note).
|
|
81
97
|
* Binding matchers should compare emails case-insensitively.
|
|
98
|
+
*
|
|
99
|
+
* A creator with no email whose id is the "system" audit placeholder is
|
|
100
|
+
* NOT a principal (see SYSTEM_CREATOR_SENTINEL) and resolves to
|
|
101
|
+
* anonymous. Email-first is deliberate here too: a real account that
|
|
102
|
+
* merely has "system" somewhere in its id is never demoted, because its
|
|
103
|
+
* email wins before the sentinel check runs.
|
|
82
104
|
*/
|
|
83
105
|
export function resolveCallerIdentity(
|
|
84
106
|
sessionMetadata: Record<string, string> | undefined,
|
|
@@ -90,10 +112,23 @@ export function resolveCallerIdentity(
|
|
|
90
112
|
}
|
|
91
113
|
|
|
92
114
|
const email = creator?.email?.trim();
|
|
115
|
+
if (email) {
|
|
116
|
+
return { kind: STIGMER_USER_KIND, value: email };
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
const id = creator?.id?.trim();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
if (id === SYSTEM_CREATOR_SENTINEL) {
|
|
121
|
+
// Operator tripwire: identity-gated MCP tools will refuse this
|
|
122
|
+
// session; the fix is real caller attribution, never a "system" grant.
|
|
123
|
+
console.info(
|
|
124
|
+
`Session creator is the "${SYSTEM_CREATOR_SENTINEL}" audit placeholder ` +
|
|
125
|
+
`(no email) — not a resolvable principal; presenting the anonymous ` +
|
|
126
|
+
`caller identity to MCP servers`,
|
|
127
|
+
);
|
|
128
|
+
return anonymousCallerIdentity();
|
|
129
|
+
}
|
|
130
|
+
if (id) {
|
|
131
|
+
return { kind: STIGMER_USER_KIND, value: id };
|
|
97
132
|
}
|
|
98
133
|
|
|
99
134
|
return anonymousCallerIdentity();
|
|
@@ -29,9 +29,17 @@ async function serializeForProxy(obj: unknown): Promise<{ type: string; binary:
|
|
|
29
29
|
describe("HttpCheckpointSaver", () => {
|
|
30
30
|
let saver: HttpCheckpointSaver;
|
|
31
31
|
let fetchSpy: ReturnType<typeof vi.fn>;
|
|
32
|
+
let recordedDelays: number[];
|
|
32
33
|
|
|
33
34
|
beforeEach(() => {
|
|
34
|
-
|
|
35
|
+
// delayFn injected so retry-path tests record the backoff schedule
|
|
36
|
+
// instead of sleeping through it (~7.75 s of real delays otherwise).
|
|
37
|
+
recordedDelays = [];
|
|
38
|
+
saver = new HttpCheckpointSaver(PROXY, TOKEN, {
|
|
39
|
+
delayFn: async (ms) => {
|
|
40
|
+
recordedDelays.push(ms);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
35
43
|
fetchSpy = vi.fn();
|
|
36
44
|
vi.stubGlobal("fetch", fetchSpy);
|
|
37
45
|
});
|
|
@@ -163,6 +171,193 @@ describe("HttpCheckpointSaver", () => {
|
|
|
163
171
|
});
|
|
164
172
|
});
|
|
165
173
|
|
|
174
|
+
// The bounded-backoff loop (cloud#188). Classification policy itself is
|
|
175
|
+
// covered by shared/__tests__/http-retry.test.ts; these cases pin the
|
|
176
|
+
// loop's behavior at the saver's call sites: transient failures recover,
|
|
177
|
+
// deterministic failures never retry, budget exhaustion still fails
|
|
178
|
+
// loudly, and 404 semantics survive unchanged.
|
|
179
|
+
describe("retry behavior", () => {
|
|
180
|
+
const ok404 = { status: 404, ok: false, statusText: "Not Found" };
|
|
181
|
+
const err503 = { status: 503, ok: false, statusText: "Service Unavailable" };
|
|
182
|
+
const err400 = { status: 400, ok: false, statusText: "Bad Request" };
|
|
183
|
+
const err401 = { status: 401, ok: false, statusText: "Unauthorized" };
|
|
184
|
+
|
|
185
|
+
function makeCheckpoint(): Checkpoint {
|
|
186
|
+
return {
|
|
187
|
+
v: 4,
|
|
188
|
+
id: "cp-retry",
|
|
189
|
+
ts: new Date().toISOString(),
|
|
190
|
+
channel_values: {},
|
|
191
|
+
channel_versions: {},
|
|
192
|
+
versions_seen: {},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const md: CheckpointMetadata = { source: "loop", step: 0, parents: {} };
|
|
196
|
+
|
|
197
|
+
it("recovers a put from a transient 503", async () => {
|
|
198
|
+
fetchSpy
|
|
199
|
+
.mockResolvedValueOnce(err503)
|
|
200
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) });
|
|
201
|
+
|
|
202
|
+
const result = await saver.put(makeConfig(), makeCheckpoint(), md, {});
|
|
203
|
+
expect(result.configurable?.checkpoint_id).toBe("cp-retry");
|
|
204
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
205
|
+
expect(recordedDelays).toEqual([250]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("recovers a put from a network-level rejection (undici TypeError)", async () => {
|
|
209
|
+
fetchSpy
|
|
210
|
+
.mockRejectedValueOnce(new TypeError("fetch failed"))
|
|
211
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) });
|
|
212
|
+
|
|
213
|
+
await expect(saver.put(makeConfig(), makeCheckpoint(), md, {})).resolves.toBeDefined();
|
|
214
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("backs off exponentially across consecutive transient failures", async () => {
|
|
218
|
+
fetchSpy
|
|
219
|
+
.mockResolvedValueOnce(err503)
|
|
220
|
+
.mockResolvedValueOnce(err503)
|
|
221
|
+
.mockResolvedValueOnce(err503)
|
|
222
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) });
|
|
223
|
+
|
|
224
|
+
await saver.put(makeConfig(), makeCheckpoint(), md, {});
|
|
225
|
+
expect(recordedDelays).toEqual([250, 500, 1000]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("fails loudly when the budget is exhausted on a retryable status", async () => {
|
|
229
|
+
fetchSpy.mockResolvedValue(err503);
|
|
230
|
+
|
|
231
|
+
await expect(saver.put(makeConfig(), makeCheckpoint(), md, {}))
|
|
232
|
+
.rejects.toThrow("Checkpoint PUT failed: 503");
|
|
233
|
+
// 1 initial attempt + 5 retries (the default budget).
|
|
234
|
+
expect(fetchSpy).toHaveBeenCalledTimes(6);
|
|
235
|
+
expect(recordedDelays).toEqual([250, 500, 1000, 2000, 4000]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("rethrows the original error when the budget is exhausted on network rejections", async () => {
|
|
239
|
+
const netErr = new TypeError("fetch failed");
|
|
240
|
+
fetchSpy.mockRejectedValue(netErr);
|
|
241
|
+
|
|
242
|
+
await expect(saver.putWrites(makeConfig(), [["messages", { a: 1 }]], "t1"))
|
|
243
|
+
.rejects.toBe(netErr);
|
|
244
|
+
expect(fetchSpy).toHaveBeenCalledTimes(6);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("never retries a deterministic 400", async () => {
|
|
248
|
+
fetchSpy.mockResolvedValue(err400);
|
|
249
|
+
|
|
250
|
+
await expect(saver.put(makeConfig(), makeCheckpoint(), md, {}))
|
|
251
|
+
.rejects.toThrow("Checkpoint PUT failed: 400");
|
|
252
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
253
|
+
expect(recordedDelays).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("never retries a rejected credential (401)", async () => {
|
|
257
|
+
fetchSpy.mockResolvedValue(err401);
|
|
258
|
+
|
|
259
|
+
await expect(saver.getTuple(makeConfig())).rejects.toThrow("Checkpoint GET failed: 401");
|
|
260
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("never retries a manual abort (AbortError)", async () => {
|
|
264
|
+
const abortErr = new DOMException("This operation was aborted", "AbortError");
|
|
265
|
+
fetchSpy.mockRejectedValue(abortErr);
|
|
266
|
+
|
|
267
|
+
await expect(saver.getTuple(makeConfig())).rejects.toBe(abortErr);
|
|
268
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("retries a hung request aborted by the per-request timeout (TimeoutError)", async () => {
|
|
272
|
+
fetchSpy
|
|
273
|
+
.mockRejectedValueOnce(
|
|
274
|
+
new DOMException("The operation was aborted due to timeout", "TimeoutError"),
|
|
275
|
+
)
|
|
276
|
+
.mockResolvedValueOnce(ok404);
|
|
277
|
+
|
|
278
|
+
await expect(saver.getTuple(makeConfig())).resolves.toBeUndefined();
|
|
279
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("preserves getTuple's 404 semantics without retrying", async () => {
|
|
283
|
+
fetchSpy.mockResolvedValue(ok404);
|
|
284
|
+
|
|
285
|
+
await expect(saver.getTuple(makeConfig())).resolves.toBeUndefined();
|
|
286
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("preserves deleteThread's 404 tolerance without retrying", async () => {
|
|
290
|
+
fetchSpy.mockResolvedValue(ok404);
|
|
291
|
+
|
|
292
|
+
await expect(saver.deleteThread("thread-1")).resolves.toBeUndefined();
|
|
293
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("bounds every request with an abort signal", async () => {
|
|
297
|
+
fetchSpy.mockResolvedValue(ok404);
|
|
298
|
+
|
|
299
|
+
await saver.getTuple(makeConfig());
|
|
300
|
+
const [, opts] = fetchSpy.mock.calls[0];
|
|
301
|
+
expect(opts.signal).toBeInstanceOf(AbortSignal);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("surfaces a pending-writes read failure instead of silently degrading", async () => {
|
|
305
|
+
// The checkpoint GET succeeds; the follow-up writes GET fails on every
|
|
306
|
+
// attempt. Pre-#188 code mapped this to "no pending writes" and
|
|
307
|
+
// returned a tuple stripped of resume state.
|
|
308
|
+
const cp = makeCheckpoint();
|
|
309
|
+
const cpSer = await serializeForProxy(cp);
|
|
310
|
+
const mdSer = await serializeForProxy(md);
|
|
311
|
+
const checkpointDoc = {
|
|
312
|
+
thread_id: "thread-1",
|
|
313
|
+
checkpoint_ns: "",
|
|
314
|
+
checkpoint_id: cp.id,
|
|
315
|
+
type: cpSer.type,
|
|
316
|
+
checkpoint: cpSer.binary,
|
|
317
|
+
metadata_type: mdSer.type,
|
|
318
|
+
metadata: mdSer.binary,
|
|
319
|
+
};
|
|
320
|
+
fetchSpy.mockImplementation((url: string) => {
|
|
321
|
+
if (url.includes("/writes")) return Promise.resolve(err503);
|
|
322
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(checkpointDoc) });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await expect(saver.getTuple(makeConfig())).rejects.toThrow(
|
|
326
|
+
"Checkpoint writes GET failed: 503",
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("recovers the pending-writes read from a transient failure", async () => {
|
|
331
|
+
const cp = makeCheckpoint();
|
|
332
|
+
const cpSer = await serializeForProxy(cp);
|
|
333
|
+
const mdSer = await serializeForProxy(md);
|
|
334
|
+
const checkpointDoc = {
|
|
335
|
+
thread_id: "thread-1",
|
|
336
|
+
checkpoint_ns: "",
|
|
337
|
+
checkpoint_id: cp.id,
|
|
338
|
+
type: cpSer.type,
|
|
339
|
+
checkpoint: cpSer.binary,
|
|
340
|
+
metadata_type: mdSer.type,
|
|
341
|
+
metadata: mdSer.binary,
|
|
342
|
+
};
|
|
343
|
+
let writesCalls = 0;
|
|
344
|
+
fetchSpy.mockImplementation((url: string) => {
|
|
345
|
+
if (url.includes("/writes")) {
|
|
346
|
+
writesCalls++;
|
|
347
|
+
return writesCalls === 1
|
|
348
|
+
? Promise.resolve(err503)
|
|
349
|
+
: Promise.resolve({ ok: true, json: () => Promise.resolve({ writes: [] }) });
|
|
350
|
+
}
|
|
351
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(checkpointDoc) });
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const tuple = await saver.getTuple(makeConfig());
|
|
355
|
+
expect(tuple?.checkpoint.id).toBe(cp.id);
|
|
356
|
+
expect(tuple?.pendingWrites).toEqual([]);
|
|
357
|
+
expect(writesCalls).toBe(2);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
166
361
|
describe("URL construction", () => {
|
|
167
362
|
it("strips trailing slashes from proxy endpoint", () => {
|
|
168
363
|
const s = new HttpCheckpointSaver("https://proxy.test///", "tok");
|
|
@@ -13,6 +13,19 @@
|
|
|
13
13
|
*
|
|
14
14
|
* The Java proxy calls Document.parse(json) which handles $binary
|
|
15
15
|
* natively, and doc.toJson() emits the same format on reads.
|
|
16
|
+
*
|
|
17
|
+
* Every request runs through the shared bounded-backoff loop
|
|
18
|
+
* (http-retry.ts's fetchWithRetry, with this saver's policy: transient
|
|
19
|
+
* classification + a per-request abort timeout). That loop is the ONLY
|
|
20
|
+
* retry layer between an agent execution and its checkpoints — the deep
|
|
21
|
+
* agent activity is deliberately non-retryable at the Temporal level
|
|
22
|
+
* (maximumAttempts: 1; replaying a whole agent run is not safe), so before
|
|
23
|
+
* this existed a single dropped request killed the execution
|
|
24
|
+
* (stigmer/stigmer-cloud#188). Retrying puts is safe by server contract:
|
|
25
|
+
* CheckpointStore documents putCheckpoint/putWrite as keyed save-or-replace
|
|
26
|
+
* upserts. Budget exhaustion still fails loudly — checkpoints are not
|
|
27
|
+
* lossy-tolerable (unlike status updates, see status.ts), and silently
|
|
28
|
+
* dropping one would corrupt resume state.
|
|
16
29
|
*/
|
|
17
30
|
|
|
18
31
|
import type { RunnableConfig } from "@langchain/core/runnables";
|
|
@@ -25,6 +38,8 @@ import {
|
|
|
25
38
|
type ChannelVersions,
|
|
26
39
|
} from "@langchain/langgraph-checkpoint";
|
|
27
40
|
import type { CheckpointMetadata, PendingWrite } from "@langchain/langgraph-checkpoint";
|
|
41
|
+
import type { RetryOptions } from "../grpc-retry.js";
|
|
42
|
+
import { fetchWithRetry, type FetchRetryPolicy } from "../http-retry.js";
|
|
28
43
|
|
|
29
44
|
function encodeB64(data: Uint8Array): string {
|
|
30
45
|
if (typeof Buffer !== "undefined") {
|
|
@@ -77,17 +92,59 @@ function configOrg(config: RunnableConfig): string | undefined {
|
|
|
77
92
|
return config.configurable?.org as string | undefined;
|
|
78
93
|
}
|
|
79
94
|
|
|
95
|
+
/** Retry policy plus the per-request bound; every field is defaulted. */
|
|
96
|
+
export interface HttpCheckpointSaverOptions extends RetryOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Milliseconds before an in-flight request is aborted and the attempt is
|
|
99
|
+
* classified retryable. Without it a hung connection (the realistic
|
|
100
|
+
* unplanned-pod-kill symptom) stalls forever and the retry never engages.
|
|
101
|
+
*/
|
|
102
|
+
readonly requestTimeoutMs?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Default transient-retry policy. Wider than status.ts's DEFAULT_PERSIST_RETRY
|
|
107
|
+
* (3 retries, ~700 ms) on purpose: a dropped status update is lossy-tolerable,
|
|
108
|
+
* a dropped checkpoint kills the execution, so the budget here is sized to
|
|
109
|
+
* span a pod-restart reroute (~7.75 s of delays across 5 retries). Worst case
|
|
110
|
+
* with every attempt hanging — 6 x 30 s + delays, ~3.7 min — stays well inside
|
|
111
|
+
* the deep-agent activity's 1 h startToCloseTimeout (no heartbeatTimeout).
|
|
112
|
+
*/
|
|
113
|
+
const DEFAULT_RETRY = {
|
|
114
|
+
baseDelayMs: 250,
|
|
115
|
+
backoffFactor: 2,
|
|
116
|
+
maxRetries: 5,
|
|
117
|
+
requestTimeoutMs: 30_000,
|
|
118
|
+
} as const;
|
|
119
|
+
|
|
80
120
|
export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
81
121
|
private readonly baseUrl: string;
|
|
82
122
|
private readonly headers: Record<string, string>;
|
|
123
|
+
private readonly retryPolicy: FetchRetryPolicy;
|
|
83
124
|
|
|
84
|
-
constructor(
|
|
125
|
+
constructor(
|
|
126
|
+
proxyEndpoint: string,
|
|
127
|
+
authToken: string,
|
|
128
|
+
options: HttpCheckpointSaverOptions = {},
|
|
129
|
+
) {
|
|
85
130
|
super();
|
|
86
131
|
this.baseUrl = `${proxyEndpoint.replace(/\/+$/, "")}/v1/proxy/checkpoints`;
|
|
87
132
|
this.headers = {
|
|
88
133
|
Authorization: `Bearer ${authToken}`,
|
|
89
134
|
"Content-Type": "application/json",
|
|
90
135
|
};
|
|
136
|
+
this.retryPolicy = {
|
|
137
|
+
label: "HttpCheckpointSaver",
|
|
138
|
+
baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,
|
|
139
|
+
backoffFactor: options.backoffFactor ?? DEFAULT_RETRY.backoffFactor,
|
|
140
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY.maxRetries,
|
|
141
|
+
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_RETRY.requestTimeoutMs,
|
|
142
|
+
delayFn: options.delayFn,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private fetchWithRetry(url: string, init?: RequestInit): Promise<Response> {
|
|
147
|
+
return fetchWithRetry(url, init, this.retryPolicy);
|
|
91
148
|
}
|
|
92
149
|
|
|
93
150
|
private async serializeTyped(obj: unknown): Promise<[string, BinaryObj]> {
|
|
@@ -113,7 +170,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
113
170
|
params.set("checkpoint_id", checkpointId);
|
|
114
171
|
}
|
|
115
172
|
|
|
116
|
-
const resp = await
|
|
173
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/checkpoint?${params}`, {
|
|
117
174
|
headers: this.headers,
|
|
118
175
|
});
|
|
119
176
|
if (resp.status === 404) return undefined;
|
|
@@ -143,7 +200,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
143
200
|
params.set("before", beforeId);
|
|
144
201
|
}
|
|
145
202
|
|
|
146
|
-
const resp = await
|
|
203
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/checkpoints?${params}`, {
|
|
147
204
|
headers: this.headers,
|
|
148
205
|
});
|
|
149
206
|
if (!resp.ok) {
|
|
@@ -183,7 +240,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
183
240
|
const orgId = configOrg(config);
|
|
184
241
|
if (orgId) doc.org_id = orgId;
|
|
185
242
|
|
|
186
|
-
const resp = await
|
|
243
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/checkpoint`, {
|
|
187
244
|
method: "PUT",
|
|
188
245
|
headers: this.headers,
|
|
189
246
|
body: JSON.stringify(doc),
|
|
@@ -203,7 +260,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
203
260
|
|
|
204
261
|
async deleteThread(threadId: string): Promise<void> {
|
|
205
262
|
const params = new URLSearchParams({ thread_id: threadId });
|
|
206
|
-
const resp = await
|
|
263
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/thread?${params}`, {
|
|
207
264
|
method: "DELETE",
|
|
208
265
|
headers: this.headers,
|
|
209
266
|
});
|
|
@@ -238,7 +295,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
238
295
|
return doc;
|
|
239
296
|
}));
|
|
240
297
|
|
|
241
|
-
const resp = await
|
|
298
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/writes`, {
|
|
242
299
|
method: "PUT",
|
|
243
300
|
headers: this.headers,
|
|
244
301
|
body: JSON.stringify({ writes: docs }),
|
|
@@ -272,7 +329,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
272
329
|
};
|
|
273
330
|
}
|
|
274
331
|
|
|
275
|
-
const writesResp = await
|
|
332
|
+
const writesResp = await this.fetchWithRetry(
|
|
276
333
|
`${this.baseUrl}/writes?${new URLSearchParams({
|
|
277
334
|
thread_id: threadId,
|
|
278
335
|
checkpoint_ns: checkpointNs,
|
|
@@ -280,8 +337,14 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
280
337
|
})}`,
|
|
281
338
|
{ headers: this.headers },
|
|
282
339
|
);
|
|
340
|
+
// Fail loudly, never degrade: this used to map ANY non-ok response to "no
|
|
341
|
+
// pending writes", which silently stripped resume state on a transient
|
|
342
|
+
// 500 — a checkpoint would load without the writes recorded against it.
|
|
343
|
+
if (!writesResp.ok) {
|
|
344
|
+
throw new Error(`Checkpoint writes GET failed: ${writesResp.status} ${writesResp.statusText}`);
|
|
345
|
+
}
|
|
283
346
|
const pendingWrites = await this.parseWrites(
|
|
284
|
-
|
|
347
|
+
(await writesResp.json()) as Record<string, any>,
|
|
285
348
|
);
|
|
286
349
|
|
|
287
350
|
return {
|
|
@@ -105,6 +105,7 @@ export function synthesizeConversationAttachment(
|
|
|
105
105
|
slug: CONVERSATION_ATTACHMENT_SLUG,
|
|
106
106
|
toolApprovals: [],
|
|
107
107
|
pinnedToolApprovals: [],
|
|
108
|
+
toolApprovalOverrides: [],
|
|
108
109
|
discoveredCapabilitiesEmpty: false,
|
|
109
110
|
connectionType: "http",
|
|
110
111
|
url: options.bridgeEndpoint.replace(/\/+$/, "") + CONVERSATION_ROUTE,
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conversation catchup (cloud channel-conversations DD-006): what happened on
|
|
3
3
|
* a live channel conversation that the agent has not seen — customer messages
|
|
4
|
-
* handled by a human teammate, the teammate's replies, platform notices
|
|
5
|
-
*
|
|
4
|
+
* handled by a human teammate, the teammate's replies, platform notices,
|
|
5
|
+
* notes, and the agent's own earlier escalations. Since cloud#347 a line
|
|
6
|
+
* whose send is known-undelivered carries a `(not delivered)` speaker
|
|
7
|
+
* annotation and an in-flight one carries `(sending)` — the digest no longer
|
|
8
|
+
* implies every public-lane line reached the customer. Since cloud#352 the
|
|
9
|
+
* digest also closes the feedback loop on send OUTCOMES: the agent's own
|
|
10
|
+
* dead-lettered sends surface as `You (not delivered):` lines, and a send
|
|
11
|
+
* that failed only after an earlier digest already conveyed it surfaces as a
|
|
12
|
+
* `System:` delivery-failure notice at the moment the failure became known.
|
|
6
13
|
*
|
|
7
14
|
* The cloud composes the CONTENT (bare `Customer:` / `Teammate:` / `System:` /
|
|
8
15
|
* `You escalated:` / `Note:` lines, oldest first) on the execution spec's
|
|
@@ -29,16 +36,42 @@ import type { ConversationCatchup } from "@stigmer/protos/ai/stigmer/agentic/age
|
|
|
29
36
|
* drift between them. Deliberately takeover-neutral: a digest can exist with
|
|
30
37
|
* no human handoff at all (a failed turn's re-composed window), so the
|
|
31
38
|
* preamble asserts only what is always true (the A15/A20 honesty bar).
|
|
39
|
+
*
|
|
40
|
+
* The send-status annotations (cloud#347) get an explicit exception from the
|
|
41
|
+
* don't-re-answer contract: a `(not delivered)` teammate reply is words the
|
|
42
|
+
* customer never got, so treating it as settled history would silently
|
|
43
|
+
* abandon whatever it was meant to convey. The preamble defines the
|
|
44
|
+
* annotations — the DD-013 split puts model-facing meaning here, while the
|
|
45
|
+
* cloud composer owns which lines earn them.
|
|
46
|
+
*
|
|
47
|
+
* The send-outcome lines (cloud#352, cloud triage DD-009) extend the same
|
|
48
|
+
* exception to the agent's OWN sends: `You (not delivered):` lines and
|
|
49
|
+
* `System:` delivery-failure notices mean the customer never got those
|
|
50
|
+
* words. The behavioral contract is owner-ruled (DD-009 Q-4): treat the
|
|
51
|
+
* failure as unfinished conversation business — weigh what still needs
|
|
52
|
+
* saying and re-say it naturally in the next turn — but never resend the
|
|
53
|
+
* failed text verbatim: a multi-chunk send can partially land, and a
|
|
54
|
+
* word-for-word repeat risks the customer reading the same message twice.
|
|
32
55
|
*/
|
|
33
56
|
const CONVERSATION_CATCHUP_PREAMBLE =
|
|
34
57
|
"Below is activity from this conversation that you have not seen — " +
|
|
35
58
|
"oldest first. It may include customer messages that were handled by a " +
|
|
36
|
-
"human teammate, the teammate's own replies, notices the
|
|
37
|
-
"
|
|
59
|
+
"human teammate, the teammate's own replies, notices sent to the " +
|
|
60
|
+
"customer, internal notes, and escalations you raised earlier. Treat it " +
|
|
38
61
|
"as conversation history you already know: do not answer or re-answer " +
|
|
39
62
|
"these messages, do not repeat or summarize them back, and do not " +
|
|
40
|
-
"mention any handoff unless asked.
|
|
41
|
-
"
|
|
63
|
+
"mention any handoff unless asked. One exception: lines marked " +
|
|
64
|
+
"(not delivered) never reached the customer, and lines marked (sending) " +
|
|
65
|
+
"were still on their way when this summary was built — the customer may " +
|
|
66
|
+
"not have seen those words, so weigh that when deciding what still needs " +
|
|
67
|
+
"saying. That includes your own words: a line marked " +
|
|
68
|
+
"You (not delivered), or a System line reporting that a message was not " +
|
|
69
|
+
"delivered, means the customer never received it. Treat such a failure " +
|
|
70
|
+
"as unfinished business — if what it said still matters, work it " +
|
|
71
|
+
"naturally into your reply in your own words, but never resend the " +
|
|
72
|
+
"failed text word-for-word (part of it may have reached the customer, " +
|
|
73
|
+
"and an exact repeat reads as a duplicate). " +
|
|
74
|
+
"Continue from the customer's newest message.";
|
|
42
75
|
|
|
43
76
|
/**
|
|
44
77
|
* Read the catchup digest from an execution spec's `conversation_catchup`.
|