@sema-agent/server 7.43.0 → 7.44.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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -1,125 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P1 ①② — the async-workflow COMPLETION INBOX (the deployment half of "route a finished
|
|
3
|
-
* background workflow back to the session that started it").
|
|
4
|
-
*
|
|
5
|
-
* ## Why this exists
|
|
6
|
-
* A self-orchestration / background workflow (started via core's `RunWorkflow`) runs to terminal OUTSIDE the
|
|
7
|
-
* originating session's request loop — often minutes later, possibly after that turn's SSE stream already
|
|
8
|
-
* closed. core's notifier seam + the SVC-1 {@link WorkflowNotifyGate} deliver its terminal payload to the
|
|
9
|
-
* deployment's `deliverWorkflowCompletion` AT-LEAST-ONCE, but that was a log-only shell: the model that
|
|
10
|
-
* launched the workflow never learned it finished (it had only the deterministic `WorkflowStatus` poll floor).
|
|
11
|
-
*
|
|
12
|
-
* This inbox is the PUSH half. `deliverWorkflowCompletion` resolves the originating **session + owner** (from
|
|
13
|
-
* the payload's `sourceTaskId`) and `enqueue`s the completion here; when that session next OPENS a stream (a
|
|
14
|
-
* new `POST /v1/runs` turn), the HTTP layer `drain`s the inbox and — for entries whose `owner` matches the
|
|
15
|
-
* draining caller's VERIFIED principal — emits a `workflow_complete` out-of-band SSE frame (mirroring the
|
|
16
|
-
* question/elicit frame pattern) that the shell turns into a `<task-notification>` (the shell's
|
|
17
|
-
* `enqueuePendingNotification`, deduped by runId). Delivery is thus decoupled from whether a stream was open
|
|
18
|
-
* at completion time — a disconnected-then-reconnected shell still gets it — WITHOUT a cross-loop append into
|
|
19
|
-
* the originating run's own event log (its `seq` counter is owned by that run's loop; appending from outside
|
|
20
|
-
* would race it — so completions live in this SEPARATE store).
|
|
21
|
-
*
|
|
22
|
-
* ## Tenancy
|
|
23
|
-
* Each entry carries the originating run's `owner`. The drain-side owner check (in the HTTP layer) is a
|
|
24
|
-
* defence-in-depth SECOND gate on top of the authorizer's sessionId→owner binding: a session that is deleted
|
|
25
|
-
* then re-claimed by a different tenant rotates that binding, so the stamped `owner` is what actually gates
|
|
26
|
-
* whether a completion is EMITTED. `purge(sessionId)` is called on session delete so orphaned completions
|
|
27
|
-
* don't linger. (`owner: null` = the single-user turnkey posture, no tenancy boundary.)
|
|
28
|
-
*
|
|
29
|
-
* ## Durability / delivery model
|
|
30
|
-
* - **At-least-once, shell-deduped**: the shell dedups by `runId` (`markTaskNotified`), so a completion frame
|
|
31
|
-
* re-delivered across a crash is harmless. `drain` therefore removes on read; the small window where a crash
|
|
32
|
-
* after `drain` but before the frame is flushed loses a push is covered by the deterministic `WorkflowStatus`
|
|
33
|
-
* poll floor (core 1.197) — never a silent hang.
|
|
34
|
-
* - **Crash-safe File ledger** (default): an append-only `(op) → record` journal, replayed into an in-memory
|
|
35
|
-
* `sessionId → (runId → entry)` map. A torn trailing line (crash mid-append) is dropped on replay
|
|
36
|
-
* (uncommitted). **Compacted** — rewritten from the live in-memory state on boot AND once the ledger grows
|
|
37
|
-
* past {@link COMPACT_EVERY} appends — so tombstones/evicted/drained lines don't accumulate without bound.
|
|
38
|
-
* Owner-only perms (identity data).
|
|
39
|
-
* - **Bounded**: at most {@link MAX_PENDING_PER_SESSION} newest completions are retained per session; the
|
|
40
|
-
* oldest is dropped (with a warn on LIVE enqueue — suppressed during replay to avoid log noise).
|
|
41
|
-
*/
|
|
42
1
|
import { mkdirSync, readFileSync, existsSync, openSync, writeSync, fsyncSync, closeSync, renameSync } from "node:fs";
|
|
43
2
|
import { join } from "node:path";
|
|
44
3
|
import { redactSecrets } from "../trace/redact.js";
|
|
45
|
-
/** Cap on retained pending completions per session (drop-oldest beyond it). A session with more than this many
|
|
46
|
-
* undelivered workflow completions is pathological; the poll floor still covers the dropped ones. */
|
|
47
4
|
export const MAX_PENDING_PER_SESSION = 100;
|
|
48
|
-
/** Compact the File ledger (rewrite from live state) once this many records have been appended since the last
|
|
49
|
-
* compaction — bounds on-disk size + boot replay cost for a long-lived process with high workflow churn. */
|
|
50
5
|
export const COMPACT_EVERY = 512;
|
|
51
|
-
/** How long after a `purge` an `enqueue` for the same session is refused (the purge-vs-enqueue fence): a
|
|
52
|
-
* workflow completion landing AFTER its session was deleted must not resurrect as a live entry the next
|
|
53
|
-
* claimant of that sessionId would see. In-memory only (a restart forgets the fence — the cross-restart race
|
|
54
|
-
* window is negligible and the drain-side owner gate still stands as the real tenancy boundary). */
|
|
55
6
|
export const PURGE_FENCE_MS = 10 * 60 * 1000;
|
|
56
|
-
/** How long after {@link WorkflowCompletionInbox.markTerminalServed} an `enqueue` for the SAME (session,
|
|
57
|
-
* runId) is refused — covers the notify path lagging the model's poll (the notify gate is async +
|
|
58
|
-
* at-least-once, so the completion can land in the inbox AFTER the poll already told the model). Long enough
|
|
59
|
-
* to swallow any realistic notify lag incl. a boot-recovery re-fire racing a fresh poll; per-runId scoped, so
|
|
60
|
-
* it can never starve a DIFFERENT workflow's push. In-memory only (see {@link PURGE_FENCE_MS} rationale). */
|
|
61
7
|
export const SERVED_FENCE_MS = 30 * 60 * 1000;
|
|
62
|
-
/** design/144 §4 server 折叠键:`JSON([域, taskId, RAW status])[#seq]`。域=external 帧独立 lane(task_id 是
|
|
63
|
-
* 调用方自由串,不分域可预占内部帧键把真实子代 settle 帧吞掉)/其余内部帧统一 "task" 域。
|
|
64
|
-
*
|
|
65
|
-
* 三路复审修1(MED):status 保留 RAW,不再归一 completed/failed —— 归一让同 id 无 seq 的两条 external
|
|
66
|
-
* 通知(先 `event` 后 `cancelled`)折叠成同一把 `…:failed` 键,inbox map 里第二条 REPLACE 第一条,drain
|
|
67
|
-
* 只吐一条(entry 的存储 status 列仍归一,那是账本的双终态列,不动;RAW status 本就随 payload extras
|
|
68
|
-
* 透传)。这与 {@link taskNotificationStreamKey} 的 RAW 语义对齐(stream 侧早已保 RAW)。
|
|
69
|
-
*
|
|
70
|
-
* 编码=防碰撞元组:域/taskId/status 走 JSON 数组(调用方自由串伪造不了分隔符);`#seq` 尾折在 JSON 之外
|
|
71
|
-
* ——JSON 段恒以 `"]` 结尾,`#digits` 尾无歧义,于是 {@link foldKeyFamily} 的剥尾语义 AND SQL 双生子
|
|
72
|
-
* (workflow-run-store-sql 的 `regexp_replace(run_id, '#[0-9]+$', '')` 家族比对)原样成立——把 seq 折进
|
|
73
|
-
* JSON 元组会让 SQL 侧家族 ack/fence 永 miss(那是另一批的真库改动面,本修有意不碰)。
|
|
74
|
-
* seq 折入=逐停机周期各自成键(1.283 起 background_agent 四腿 notify 铸 cycleSeq);无 seq=单周期旧键
|
|
75
|
-
* (bash/monitor terminal/retain 降级,诚实语义)。serve-fence 侧必须用同一函数(或其家族前缀)构造,
|
|
76
|
-
* 否则键形漂移=fence 永 miss(server.ts fleet 腿的 `taskNotificationFoldKey({task_id, status})` 无 seq 形
|
|
77
|
-
* 即家族前缀,两侧同为 RAW status,依旧咬合)。 */
|
|
78
8
|
export function taskNotificationFoldKey(n) {
|
|
79
9
|
const domain = n.task_type === "external" ? "external" : "task";
|
|
80
10
|
return `${JSON.stringify([domain, n.task_id, n.status])}${n.seq !== undefined ? `#${n.seq}` : ""}`;
|
|
81
11
|
}
|
|
82
|
-
/** design/144 stream 侧去重键(core [784]④a + codex 1.189 前双审 F2)= `域:taskId:status[#seq]`。
|
|
83
|
-
* 与 {@link taskNotificationFoldKey} 同族,域二分也一致(codex F2 HIGH:external 通知的 task_id 是
|
|
84
|
-
* POST /v1/sessions/:id/notify 调用方自由串——同一条腿上,drain 出的 external 帧可与内部任务帧同形
|
|
85
|
-
* `task_id:status:seq` 撞键互吞,per-leg 键集并不豁免这个面,旧注释「没有跨调用方预占面」判错);
|
|
86
|
-
* 仍与 fold key 有意不同的一点:status 保留 RAW(killed/cancelled/event 不归一 failed)——归一会让同
|
|
87
|
-
* task 的 Monitor event 批与 failed 终态在一条腿上互吞;wire 去重要的是逐帧身份,不是 inbox 的双终态列。
|
|
88
|
-
* `seq` 折入 = 停→唤→停的第二周期同 status 帧各自成键(core 1.283 起 background_agent 四腿 notify 铸
|
|
89
|
-
* cycleSeq;缺 seq 时旧键曾把二周期 completed 帧在 observer 臂反向吞掉——core [784]④a);无 seq 的帧
|
|
90
|
-
* (bash/monitor terminal/retain 降级)保持单周期旧键=同键第二帧吞,诚实语义。[2373]B-10:core 4.0.0 起
|
|
91
|
-
* task_type 是必填闭合枚举——`task_type?` 可选性**不再对应旧 core 帧**,它活着只为家族前缀构造
|
|
92
|
-
* (server.ts fleet 腿 `taskNotificationFoldKey({task_id,status})` 同款用法);缺席归 "task" 域的语义
|
|
93
|
-
* 只在前缀用途下生效。external 帧三来源齐备(notify 端点 core 铸 / inbox entry extras 存 / drain 帧
|
|
94
|
-
* spread 复原),不会漏域。observer 臂 / stream 臂 / turn-open drain 登记三处必须用本函数
|
|
95
|
-
* 构键且都传 task_type,键形漂移=去重永 miss。 */
|
|
96
12
|
export function taskNotificationStreamKey(n) {
|
|
97
13
|
const domain = n.task_type === "external" ? "external" : "task";
|
|
98
14
|
return `${domain}:${n.task_id}:${n.status}${n.seq !== undefined ? `#${n.seq}` : ""}`;
|
|
99
15
|
}
|
|
100
|
-
/** Per-leg notified-key ledger cap (codex 1.189 前双审 F3):一条超长腿(海量互异 bg 通知)不许把键集
|
|
101
|
-
* 撑到无界——超出删最老插入(Map 迭代序=插入序)。远高于任何真实 per-leg 通知量;被逐键的老通知
|
|
102
|
-
* 最坏情形=同键晚到帧多写一次(at-least-once,shell 按键 dedup 兜底),绝不吞新帧。 */
|
|
103
16
|
export const NOTIFIED_KEYS_MAX = 4096;
|
|
104
|
-
/**
|
|
105
|
-
* Per-leg delivered-notification ledger(codex 1.189 前双审 F1 HIGH 的修位)。
|
|
106
|
-
* 键 = {@link taskNotificationStreamKey};值 = 「认领这把键的那次写是否真落盘」的 promise(true=成功)。
|
|
107
|
-
* 为什么是 Map<string, Promise<boolean>> 而不是 Set:bg/resume 腿的 observer 臂是 fire-and-forget append
|
|
108
|
-
* (`void append(...)`)——旧 Set 下 append 失败(SQL blip)时键已被占而盘上无帧,core pend-drain 把同
|
|
109
|
-
* payload 重放到 stream 臂时被键吞 = 通知永丢。现在 stream 臂对已见键 `await` 认领方的真实结果,false 则
|
|
110
|
-
* 照写并把键值升为 true。sync 腿的写是同步 res.write(无异步失败窗口)、drain 臂的写被
|
|
111
|
-
* emitPendingWorkflowCompletions await(失败→不 ack→下腿重投)——两者登记 resolved(true) 即可;三腿统一
|
|
112
|
-
* 用本 Map 形状,让三对臂保持逐行孪生。
|
|
113
|
-
*/
|
|
114
17
|
export class NotifiedKeys {
|
|
115
18
|
byKey = new Map();
|
|
116
|
-
/** The pending/settled outcome of the write that claimed `key`, or undefined when unclaimed. */
|
|
117
19
|
get(key) {
|
|
118
20
|
return this.byKey.get(key);
|
|
119
21
|
}
|
|
120
|
-
/** Claim (or update) `key` with the outcome of its write. Bounded: past {@link NOTIFIED_KEYS_MAX} the
|
|
121
|
-
* oldest-inserted key is dropped (F3; an update keeps the original insertion slot — good enough for a
|
|
122
|
-
* pathology cap). */
|
|
123
22
|
set(key, written) {
|
|
124
23
|
this.byKey.set(key, written);
|
|
125
24
|
while (this.byKey.size > NOTIFIED_KEYS_MAX) {
|
|
@@ -128,27 +27,15 @@ export class NotifiedKeys {
|
|
|
128
27
|
}
|
|
129
28
|
}
|
|
130
29
|
}
|
|
131
|
-
/** 家族前缀 = 折叠键只剥 `#seq` 分量(仅当尾是纯数字才剥)。新元组键的 JSON 段恒以 `"]` 结尾、taskId/
|
|
132
|
-
* status 里天然的 # 被 JSON 引号包住,`#digits` 尾无歧义=必是 seq;旧格式遗留行(`域:task:status#seq`)
|
|
133
|
-
* 与 workflow_complete 的裸 runId 键同一语义原样兼容。 */
|
|
134
30
|
export function foldKeyFamily(runId) {
|
|
135
31
|
const i = runId.lastIndexOf("#");
|
|
136
32
|
return i > 0 && /^\d+$/.test(runId.slice(i + 1)) ? runId.slice(0, i) : runId;
|
|
137
33
|
}
|
|
138
|
-
/** Collision-safe (session, runId) fence key — ids are caller-influenced strings, so a plain separator could
|
|
139
|
-
* be forged into a cross-pair collision; JSON tuple encoding cannot. */
|
|
140
34
|
const servedKey = (sessionId, runId) => JSON.stringify([sessionId, runId]);
|
|
141
|
-
/** In-memory inbox — the parity oracle + the ephemeral opt-out (WORKFLOW_RUN_STORE=memory). No persistence:
|
|
142
|
-
* a restart forgets undelivered completions (the poll floor covers them). */
|
|
143
35
|
export class InMemoryWorkflowCompletionInbox {
|
|
144
36
|
onWarn;
|
|
145
37
|
bySession = new Map();
|
|
146
|
-
/** sessionId → {at, owner}: purge stamps; enqueues within {@link PURGE_FENCE_MS} for the SAME owner are
|
|
147
|
-
* refused. Swept on BOTH enqueue and purge (a purge-heavy, enqueue-quiet process must not accumulate
|
|
148
|
-
* stamps unboundedly — review). */
|
|
149
38
|
purgedAt = new Map();
|
|
150
|
-
/** servedKey(sessionId, runId) → stamp: terminal-poll-served fences; an enqueue for the pair within
|
|
151
|
-
* {@link SERVED_FENCE_MS} is refused (the poll already told the model). Swept alongside the purge fence. */
|
|
152
39
|
servedAt = new Map();
|
|
153
40
|
constructor(onWarn) {
|
|
154
41
|
this.onWarn = onWarn;
|
|
@@ -161,16 +48,9 @@ export class InMemoryWorkflowCompletionInbox {
|
|
|
161
48
|
if (now - at > SERVED_FENCE_MS)
|
|
162
49
|
this.servedAt.delete(key);
|
|
163
50
|
}
|
|
164
|
-
/** True (and warns) when `entry`'s session was purged within the fence window AND the fence applies to its
|
|
165
|
-
* owner: same owner as the purged session (a late completion of the DELETED session) — or an owner-less
|
|
166
|
-
* fence (caller couldn't say), conservative. A DIFFERENT owner = a new legitimate claimant of a reused
|
|
167
|
-
* sessionId → not fenced (their push must not be starved; the drain-side owner gate stays the real
|
|
168
|
-
* tenancy boundary either way). */
|
|
169
51
|
purgeFenced(entry) {
|
|
170
52
|
const now = Date.now();
|
|
171
53
|
this.sweepFence(now);
|
|
172
|
-
// Served fence FIRST (per-runId, so it can't shadow the purge fence's owner semantics): this exact
|
|
173
|
-
// completion already reached the model through its own poll — enqueueing it would be the double delivery.
|
|
174
54
|
if (this.servedAt.has(servedKey(entry.sessionId, entry.runId)) || this.servedAt.has(servedKey(entry.sessionId, foldKeyFamily(entry.runId)))) {
|
|
175
55
|
this.onWarn?.("workflow_completion_enqueue_after_poll_served", { sessionId: entry.sessionId, runId: entry.runId });
|
|
176
56
|
return true;
|
|
@@ -179,20 +59,14 @@ export class InMemoryWorkflowCompletionInbox {
|
|
|
179
59
|
if (st === undefined)
|
|
180
60
|
return false;
|
|
181
61
|
if (st.owner !== undefined && entry.owner !== st.owner)
|
|
182
|
-
return false;
|
|
62
|
+
return false;
|
|
183
63
|
this.onWarn?.("workflow_completion_enqueue_after_purge", { sessionId: entry.sessionId, runId: entry.runId });
|
|
184
64
|
return true;
|
|
185
65
|
}
|
|
186
|
-
/** Fence FIRST (an enqueue racing the poll is refused even mid-call), then drop any already-pending
|
|
187
|
-
* entry via `ack` — the durable subclass inherits this and gets a durable ack for free. */
|
|
188
66
|
async markTerminalServed(sessionId, runId) {
|
|
189
67
|
this.sweepFence(Date.now());
|
|
190
68
|
this.servedAt.set(servedKey(sessionId, runId), Date.now());
|
|
191
69
|
await this.ack(sessionId, runId);
|
|
192
|
-
// 家族 ack(design/144 消费批):fleet 常开流帧不带 seq,而 1.283 起终态 park 是 seq 键——精确 ack
|
|
193
|
-
// 必 miss ⇒ 下次开流 drain 二次注入模型。传入 runId 视作家族前缀,清掉同 (域,taskId,status) 的全部
|
|
194
|
-
// seq 键成员。取舍如实记:同家族的更早 Monitor 批可能被连带清(终态已送达后丢进度行,低害),
|
|
195
|
-
// 换取每个 bg 终态必然双投的消除;workflow_complete 键无 # 尾,家族=自身,行为不变。
|
|
196
70
|
const q = this.bySession.get(sessionId);
|
|
197
71
|
if (q) {
|
|
198
72
|
for (const key of [...q.keys()]) {
|
|
@@ -202,16 +76,14 @@ export class InMemoryWorkflowCompletionInbox {
|
|
|
202
76
|
}
|
|
203
77
|
}
|
|
204
78
|
stampPurge(sessionId, owner) {
|
|
205
|
-
this.sweepFence(Date.now());
|
|
79
|
+
this.sweepFence(Date.now());
|
|
206
80
|
this.purgedAt.set(sessionId, { at: Date.now(), owner });
|
|
207
81
|
}
|
|
208
|
-
/** Fold one entry into the map. `warn` is false during ledger replay (a historical overflow must not re-log
|
|
209
|
-
* on every boot) and true on a live enqueue. */
|
|
210
82
|
applyEnqueue(entry, warn) {
|
|
211
83
|
let q = this.bySession.get(entry.sessionId);
|
|
212
84
|
if (!q)
|
|
213
85
|
this.bySession.set(entry.sessionId, (q = new Map()));
|
|
214
|
-
q.delete(entry.runId);
|
|
86
|
+
q.delete(entry.runId);
|
|
215
87
|
q.set(entry.runId, entry);
|
|
216
88
|
while (q.size > MAX_PENDING_PER_SESSION) {
|
|
217
89
|
const oldest = q.keys().next().value;
|
|
@@ -222,16 +94,13 @@ export class InMemoryWorkflowCompletionInbox {
|
|
|
222
94
|
}
|
|
223
95
|
async enqueue(entry) {
|
|
224
96
|
if (this.purgeFenced(entry))
|
|
225
|
-
return;
|
|
97
|
+
return;
|
|
226
98
|
this.applyEnqueue(entry, true);
|
|
227
99
|
}
|
|
228
100
|
async pending(sessionId) {
|
|
229
101
|
const q = this.bySession.get(sessionId);
|
|
230
102
|
if (!q || q.size === 0)
|
|
231
103
|
return [];
|
|
232
|
-
// The served fence is armed SYNCHRONOUSLY in markTerminalServed (before its async ack
|
|
233
|
-
// lands), so filtering here closes the same-process race where a stream-open drain reads a still-pending
|
|
234
|
-
// entry while the ack write is in flight — the drain must never re-deliver what the fence says was served.
|
|
235
104
|
return [...q.values()].filter((e) => !this.servedAt.has(servedKey(e.sessionId, e.runId)) && !this.servedAt.has(servedKey(e.sessionId, foldKeyFamily(e.runId))));
|
|
236
105
|
}
|
|
237
106
|
async ack(sessionId, runId) {
|
|
@@ -247,8 +116,6 @@ export class InMemoryWorkflowCompletionInbox {
|
|
|
247
116
|
this.stampPurge(sessionId, purgedOwner);
|
|
248
117
|
}
|
|
249
118
|
}
|
|
250
|
-
/** File-backed crash-safe inbox (the durable default — a background workflow's completion survives a replica
|
|
251
|
-
* restart, the prerequisite for pushing it to a session that reconnects after the crash). */
|
|
252
119
|
export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox {
|
|
253
120
|
fsyncEnabled;
|
|
254
121
|
dir;
|
|
@@ -259,12 +126,10 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
|
|
|
259
126
|
super(onWarn);
|
|
260
127
|
this.fsyncEnabled = fsyncEnabled;
|
|
261
128
|
this.dir = join(root, "workflow-completion-inbox");
|
|
262
|
-
// 0o700 dir + 0o600 ledger: entries carry the completion summary (may reference tenant work) + sessionId +
|
|
263
|
-
// owner — owner-only, parity with the notify-journal's identity-data posture.
|
|
264
129
|
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
265
130
|
this.ledgerPath = join(this.dir, "inbox.jsonl");
|
|
266
131
|
this.replay();
|
|
267
|
-
this.compact();
|
|
132
|
+
this.compact();
|
|
268
133
|
}
|
|
269
134
|
replay() {
|
|
270
135
|
if (!existsSync(this.ledgerPath))
|
|
@@ -278,7 +143,7 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
|
|
|
278
143
|
rec = JSON.parse(line);
|
|
279
144
|
}
|
|
280
145
|
catch {
|
|
281
|
-
continue;
|
|
146
|
+
continue;
|
|
282
147
|
}
|
|
283
148
|
if (rec.op === "enqueue" && rec.entry?.sessionId && rec.entry.runId)
|
|
284
149
|
this.applyEnqueue(rec.entry, false);
|
|
@@ -286,40 +151,29 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
|
|
|
286
151
|
const q = this.bySession.get(rec.sessionId);
|
|
287
152
|
q?.delete(rec.runId);
|
|
288
153
|
if (q && q.size === 0)
|
|
289
|
-
this.bySession.delete(rec.sessionId);
|
|
154
|
+
this.bySession.delete(rec.sessionId);
|
|
290
155
|
}
|
|
291
156
|
else if (rec.op === "drain" && rec.sessionId)
|
|
292
|
-
this.bySession.delete(rec.sessionId);
|
|
157
|
+
this.bySession.delete(rec.sessionId);
|
|
293
158
|
}
|
|
294
159
|
}
|
|
295
160
|
commit(rec) {
|
|
296
161
|
if (this.fd === undefined)
|
|
297
|
-
this.fd = openSync(this.ledgerPath, "a", 0o600);
|
|
162
|
+
this.fd = openSync(this.ledgerPath, "a", 0o600);
|
|
298
163
|
writeSync(this.fd, `${JSON.stringify(rec)}\n`);
|
|
299
164
|
if (this.fsyncEnabled)
|
|
300
|
-
fsyncSync(this.fd);
|
|
165
|
+
fsyncSync(this.fd);
|
|
301
166
|
this.appendsSinceCompact++;
|
|
302
167
|
}
|
|
303
|
-
/** Compact iff we've appended enough since the last one. MUST be called AFTER the in-memory mutation the
|
|
304
|
-
* triggering commit records — compact() rewrites from `bySession`, so a compact fired BEFORE the caller
|
|
305
|
-
* applied its enqueue/delete would rewrite a stale snapshot and lose that record. */
|
|
306
168
|
maybeCompact() {
|
|
307
169
|
if (this.appendsSinceCompact >= COMPACT_EVERY)
|
|
308
170
|
this.compact();
|
|
309
171
|
}
|
|
310
|
-
/** Rewrite the ledger from the authoritative in-memory state — one `enqueue` record per LIVE pending entry —
|
|
311
|
-
* dropping every accumulated drain-tombstone + evicted/drained line. Atomic (temp + rename) so a crash
|
|
312
|
-
* mid-compact leaves the old ledger intact. */
|
|
313
172
|
compact() {
|
|
314
173
|
if (this.fd !== undefined) {
|
|
315
174
|
closeSync(this.fd);
|
|
316
175
|
this.fd = undefined;
|
|
317
176
|
}
|
|
318
|
-
// tmp 名带 pid(2026-07-28 核查修):两个进程共用同一数据根时(例:两个 memory 形引擎共用一个
|
|
319
|
-
// HOME——multi-instance 共享目录本不受支持,但 memory 形用户合理预期「无盘无锁」,却默默共享
|
|
320
|
-
// `~/.ai-agent`),固定 `.tmp` 名会互相 rename 走对方的 staging ⇒ 对方 renameSync ENOENT,而 compact
|
|
321
|
-
// 在 constructor 里同步跑 = **boot fatal**(真机复现:两只引擎并发 boot 恢复期)。带 pid 后各进程
|
|
322
|
-
// staging 独立,rename 原子替换回落到 last-writer-wins(账本内容级共享仍不支持,但不再拒启)。
|
|
323
177
|
const tmp = `${this.ledgerPath}.tmp.${process.pid}`;
|
|
324
178
|
const lines = [];
|
|
325
179
|
for (const q of this.bySession.values())
|
|
@@ -335,9 +189,7 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
|
|
|
335
189
|
finally {
|
|
336
190
|
closeSync(tfd);
|
|
337
191
|
}
|
|
338
|
-
renameSync(tmp, this.ledgerPath);
|
|
339
|
-
// fsync the DIRECTORY so the rename (a namespace change) is itself crash-durable on POSIX/macOS, not just
|
|
340
|
-
// the file contents. Best-effort: a dir that can't be opened for fsync (rare) is skipped.
|
|
192
|
+
renameSync(tmp, this.ledgerPath);
|
|
341
193
|
if (this.fsyncEnabled) {
|
|
342
194
|
try {
|
|
343
195
|
const dfd = openSync(this.dir, "r");
|
|
@@ -349,40 +201,35 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
|
|
|
349
201
|
}
|
|
350
202
|
}
|
|
351
203
|
catch {
|
|
352
|
-
/* dir fsync unsupported/failed — the rename is still namespace-atomic; skip */
|
|
353
204
|
}
|
|
354
205
|
}
|
|
355
206
|
this.appendsSinceCompact = 0;
|
|
356
207
|
}
|
|
357
208
|
async enqueue(entry) {
|
|
358
209
|
if (this.purgeFenced(entry))
|
|
359
|
-
return;
|
|
210
|
+
return;
|
|
360
211
|
this.commit({ op: "enqueue", entry });
|
|
361
212
|
this.applyEnqueue(entry, true);
|
|
362
|
-
this.maybeCompact();
|
|
213
|
+
this.maybeCompact();
|
|
363
214
|
}
|
|
364
|
-
// `pending` is the inherited non-destructive read — deliberately NO ledger write (phase 1 of two-phase
|
|
365
|
-
// delivery leaves the durable state untouched; only the per-entry `ack` mutates it, so a crash/failed
|
|
366
|
-
// frame-write between the two re-delivers on the next leg instead of silently eating the completion).
|
|
367
215
|
async ack(sessionId, runId) {
|
|
368
216
|
const q = this.bySession.get(sessionId);
|
|
369
217
|
if (!q?.has(runId))
|
|
370
|
-
return;
|
|
371
|
-
this.commit({ op: "ack", sessionId, runId });
|
|
218
|
+
return;
|
|
219
|
+
this.commit({ op: "ack", sessionId, runId });
|
|
372
220
|
q.delete(runId);
|
|
373
221
|
if (q.size === 0)
|
|
374
222
|
this.bySession.delete(sessionId);
|
|
375
|
-
this.maybeCompact();
|
|
223
|
+
this.maybeCompact();
|
|
376
224
|
}
|
|
377
225
|
async purge(sessionId, purgedOwner) {
|
|
378
|
-
this.stampPurge(sessionId, purgedOwner);
|
|
226
|
+
this.stampPurge(sessionId, purgedOwner);
|
|
379
227
|
if (!this.bySession.has(sessionId))
|
|
380
228
|
return;
|
|
381
|
-
this.commit({ op: "drain", sessionId });
|
|
229
|
+
this.commit({ op: "drain", sessionId });
|
|
382
230
|
this.bySession.delete(sessionId);
|
|
383
|
-
this.maybeCompact();
|
|
231
|
+
this.maybeCompact();
|
|
384
232
|
}
|
|
385
|
-
/** Close the ledger fd (shutdown). */
|
|
386
233
|
close() {
|
|
387
234
|
if (this.fd !== undefined) {
|
|
388
235
|
closeSync(this.fd);
|
|
@@ -390,41 +237,14 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
|
|
|
390
237
|
}
|
|
391
238
|
}
|
|
392
239
|
}
|
|
393
|
-
|
|
394
|
-
* P1 ①② delivery half, TWO-PHASE since 1.80: read the session's pending completions, emit
|
|
395
|
-
* one `workflow_complete` out-of-band frame per entry, and `ack` (remove) each entry only AFTER its emit
|
|
396
|
-
* SUCCEEDED — a failed frame write (dead socket / durable-append error) leaves the entry pending, so the next
|
|
397
|
-
* leg re-delivers instead of silently eating it (the 1.77-review "drained-but-append-failed" window, closed).
|
|
398
|
-
* Called at STREAM-OPEN of EVERY run leg (the sync `POST /v1/runs` SSE leg emits a live frame; the bg + resume
|
|
399
|
-
* legs append a durable `workflow_complete` event their tailing client replays). Best-effort throughout (the
|
|
400
|
-
* deterministic `WorkflowStatus` poll is the floor); at-least-once — the shell dedups by runId (two legs racing
|
|
401
|
-
* the same session may double-emit; same contract).
|
|
402
|
-
*
|
|
403
|
-
* 🔒 Tenancy (defence-in-depth over the authorizer's sessionId→owner gate): only entries whose stamped
|
|
404
|
-
* `owner` matches `callerPrincipal` are EMITTED — a session deleted-then-reclaimed by a different tenant
|
|
405
|
-
* (which rotates the sessionId→owner binding) can't surface the prior owner's completions. `owner: null`
|
|
406
|
-
* (single-user turnkey) has no tenancy boundary → always emitted. A non-matching entry is a DELIBERATE drop —
|
|
407
|
-
* acked (the session is gone/reclaimed for its original owner; the poll floor is their fallback), so it never
|
|
408
|
-
* wedges the queue.
|
|
409
|
-
*/
|
|
410
|
-
export async function emitPendingWorkflowCompletions(inbox, sessionId, callerPrincipal, emit,
|
|
411
|
-
/** Emit-TARGET diagnostics — WHICH leg drained (route) onto WHAT kind of connection
|
|
412
|
-
* (live SSE write vs durable event-log append), plus the enqueue→emit lag, so a "workflow completed but the
|
|
413
|
-
* shell only saw it on the next manual turn" report can be pinned to the leg that finally carried the frame
|
|
414
|
-
* (the push is stream-open-driven by design — a session with NO open leg has no push path until its next
|
|
415
|
-
* leg; the lag makes that visible instead of a mystery). Info-level, one line per delivered frame. */
|
|
416
|
-
diag) {
|
|
240
|
+
export async function emitPendingWorkflowCompletions(inbox, sessionId, callerPrincipal, emit, diag) {
|
|
417
241
|
if (!inbox || !sessionId)
|
|
418
242
|
return;
|
|
419
|
-
// Diagnostics must never influence delivery (1.108 review): a throwing log sink inside the try would
|
|
420
|
-
// misroute an already-delivered-and-acked frame into the emit_failed line — a lie in the very diagnostics
|
|
421
|
-
// this sink exists to provide. Swallow here; the drain semantics stay byte-identical with diag absent.
|
|
422
243
|
const dlog = (msg, meta) => {
|
|
423
244
|
try {
|
|
424
245
|
diag?.log(msg, meta);
|
|
425
246
|
}
|
|
426
247
|
catch {
|
|
427
|
-
/* never let diagnostics fault the drain */
|
|
428
248
|
}
|
|
429
249
|
};
|
|
430
250
|
let entries;
|
|
@@ -432,34 +252,28 @@ diag) {
|
|
|
432
252
|
entries = await inbox.pending(sessionId);
|
|
433
253
|
}
|
|
434
254
|
catch {
|
|
435
|
-
return;
|
|
255
|
+
return;
|
|
436
256
|
}
|
|
437
257
|
for (const e of entries) {
|
|
438
258
|
try {
|
|
439
259
|
if (e.owner !== null && e.owner !== callerPrincipal) {
|
|
440
|
-
await inbox.ack(sessionId, e.runId);
|
|
260
|
+
await inbox.ack(sessionId, e.runId);
|
|
441
261
|
dlog("workflow_complete_dropped_owner_mismatch", { route: diag?.route, sessionId, runId: e.runId });
|
|
442
262
|
continue;
|
|
443
263
|
}
|
|
444
264
|
if (e.kind === "task_notification") {
|
|
445
|
-
// A background subagent/bash completion parked while the session was idle — drain it as the
|
|
446
|
-
// SAME frame family the live legs emit (`task_notification`, task_id keyed), so the shell's existing
|
|
447
|
-
// renderer + dedup consume it unchanged. Extras (task_type/result/…) were bounded+redacted at enqueue.
|
|
448
265
|
let extras = {};
|
|
449
266
|
try {
|
|
450
267
|
extras = e.payload ? JSON.parse(e.payload) : {};
|
|
451
268
|
}
|
|
452
269
|
catch {
|
|
453
|
-
/* a torn payload degrades to the summary-only frame */
|
|
454
270
|
}
|
|
455
|
-
// `injected: false` AFTER the extras spread:恒量判别位([1818]§四 (b)案,与 project.ts live 投影
|
|
456
|
-
// 同义)——parked 通知更如实:park 发生恰因 leg 已死、core 的注入无处可去。
|
|
457
271
|
await emit({ type: "task_notification", task_id: e.runId, status: e.status, summary: e.summary, ...extras, injected: false });
|
|
458
272
|
}
|
|
459
273
|
else {
|
|
460
274
|
await emit({ type: "workflow_complete", runId: e.runId, status: e.status, summary: e.summary });
|
|
461
275
|
}
|
|
462
|
-
await inbox.ack(sessionId, e.runId);
|
|
276
|
+
await inbox.ack(sessionId, e.runId);
|
|
463
277
|
dlog("workflow_complete_emitted", {
|
|
464
278
|
route: diag?.route,
|
|
465
279
|
connection: diag?.connection,
|
|
@@ -470,51 +284,22 @@ diag) {
|
|
|
470
284
|
});
|
|
471
285
|
}
|
|
472
286
|
catch (err) {
|
|
473
|
-
// emit failed → NOT acked → re-delivered on the next leg (at-least-once; a dead socket / failed append
|
|
474
|
-
// must not fault the turn). An ack failure after a successful emit re-delivers too — same contract.
|
|
475
287
|
dlog("workflow_complete_emit_failed", { route: diag?.route, connection: diag?.connection, sessionId, runId: e.runId, err: err instanceof Error ? err.message : String(err) });
|
|
476
288
|
}
|
|
477
289
|
}
|
|
478
290
|
}
|
|
479
|
-
|
|
480
|
-
* The durable half is the deployment's job: map a core `TaskNotificationPayload`
|
|
481
|
-
* (background subagent / background bash completion) onto an inbox entry, for the IDLE gap — core's in-process
|
|
482
|
-
* injection covers a LIVE harness; a completion that lands after the leg ended had NO durable path (idle no-show,
|
|
483
|
-
* lost across restart). Bounded + redacted HERE (the store carries only egress-safe bytes; summary/result are
|
|
484
|
-
* UNTRUSTED free text). `status` collapses onto the inbox's completed|failed column; the ORIGINAL status rides
|
|
485
|
-
* the payload extras and overrides on drain (killed/cancelled reach the shell verbatim). Workflow-type
|
|
486
|
-
* notifications are the CALLER's job to filter (they have their own durable chain — double-enqueueing here would
|
|
487
|
-
* re-open the poll-served double-push).
|
|
488
|
-
*/
|
|
489
|
-
export function taskNotificationInboxEntry(sessionId, owner, n, enqueuedAt,
|
|
490
|
-
/** lead-b (dual-ID audit): the PARENT run's durable taskId (the uuid the shell's footer rows
|
|
491
|
-
* key on) — rides the drained frame so a consumer can bind the a*-keyed child notification to its uuid-keyed
|
|
492
|
-
* parent row without a side lookup. */
|
|
493
|
-
parentTaskId) {
|
|
291
|
+
export function taskNotificationInboxEntry(sessionId, owner, n, enqueuedAt, parentTaskId) {
|
|
494
292
|
const cap = (s, max) => (s.length > max ? `${s.slice(0, max)}…[+${s.length - max} chars]` : s);
|
|
495
293
|
const extras = {
|
|
496
294
|
task_type: n.task_type,
|
|
497
295
|
...(parentTaskId ? { parentTaskId } : {}),
|
|
498
|
-
// core 1.238: the payload's own sessionId — the stable a*↔local-uuid cross-system anchor
|
|
499
|
-
// (same handle TaskResult.sessionId/fleet rows carry). Optional (bash lane has none).
|
|
500
296
|
...(n.sessionId ? { sessionId: n.sessionId } : {}),
|
|
501
297
|
...(n.toolUseId ? { toolUseId: n.toolUseId } : {}),
|
|
502
|
-
...(n.status !== "completed" && n.status !== "failed" ? { status: n.status } : {}),
|
|
503
|
-
// core drain 案: the Monitor/stoppedBy fields must survive the PARK too, or a drained event batch
|
|
504
|
-
// arrives with no dedup discriminator (`seq` — core's taskNotificationDedupKey folds it in), no line text,
|
|
505
|
-
// and no stop attribution. Same trust posture as the live projection (project.ts): seq/stoppedBy verbatim
|
|
506
|
-
// (core counter / open enum), lines = watched-process stdout → redact + cap each line.
|
|
298
|
+
...(n.status !== "completed" && n.status !== "failed" ? { status: n.status } : {}),
|
|
507
299
|
...(n.seq !== undefined ? { seq: n.seq } : {}),
|
|
508
|
-
// 对抗评审 2026-07-11:redact BEFORE cap — capping first could bisect a secret so its residue no longer
|
|
509
|
-
// matches any redact pattern (a half-token is still a usable prefix). Order: redact, then bound.
|
|
510
300
|
...(n.lines !== undefined ? { lines: n.lines.map((l) => cap(redactSecrets(l), 1024)) } : {}),
|
|
511
301
|
...(n.stoppedBy !== undefined ? { stoppedBy: n.stoppedBy } : {}),
|
|
512
|
-
...(n.diagnostics !== undefined ? { diagnostics: cap(n.diagnostics, 4_000) } : {}),
|
|
513
|
-
// (core 1.262.0 残局字段的 PARK 半场——live 腿 1.154.0 已放行,但「用户杀 bg 子代」的
|
|
514
|
-
// 主动线恰是 between-turns,通知落这里):与 project.ts 同一 trust posture——recentSteps.tool verbatim,
|
|
515
|
-
// target/outcome UNTRUSTED(命令行/工具输出首行可为 secret 形)→ redact+cap;editedFiles.path → redact+cap
|
|
516
|
-
// (attachedFiles 姿势),edits 数字 verbatim;resumable boolean verbatim。字段挑取绝不 spread;数组防御
|
|
517
|
-
// 截断(core 上游本就 bound ≤10 步——slice 是入库前的双保险)。
|
|
302
|
+
...(n.diagnostics !== undefined ? { diagnostics: cap(n.diagnostics, 4_000) } : {}),
|
|
518
303
|
...(n.recentSteps !== undefined
|
|
519
304
|
? { recentSteps: n.recentSteps.slice(0, 10).map((s) => ({ tool: s.tool, target: cap(redactSecrets(s.target), 512), outcome: cap(redactSecrets(s.outcome), 512) })) }
|
|
520
305
|
: {}),
|
|
@@ -528,43 +313,14 @@ parentTaskId) {
|
|
|
528
313
|
return {
|
|
529
314
|
sessionId,
|
|
530
315
|
owner,
|
|
531
|
-
// 对抗评审 2026-07-11(MED):the inbox key is REPLACE-on-runId (the workflow_complete contract) — keying a
|
|
532
|
-
// task_notification by the bare task_id COLLAPSES multiple between-turns notifications of the same bg task
|
|
533
|
-
// (a Monitor event batch, then the killed terminal: the terminal REPLACED the batch, drain emitted 1 of 2).
|
|
534
|
-
// Fold `seq` into the key (core's own taskNotificationDedupKey folds seq for exactly this reason); the frame's
|
|
535
|
-
// task_id comes from the extras override below, so the wire shape is unchanged. Same-key retries still dedup.
|
|
536
|
-
// design/144 §4 键升级:(域,taskId,status,seq?)——status 进键且保 RAW(修1:归一曾让同 id 的
|
|
537
|
-
// event/cancelled 两条 external 通知互吞),external 分域([769]①),seq 折入(逐周期)。
|
|
538
|
-
// wire 形状不变(task_id 从 extras override 恢复);存储 status 列(下一行)仍归一双终态,不受键影响。
|
|
539
316
|
runId: taskNotificationFoldKey(n),
|
|
540
317
|
status: n.status === "completed" ? "completed" : "failed",
|
|
541
318
|
summary: cap(redactSecrets(n.summary ?? ""), 4000),
|
|
542
319
|
enqueuedAt,
|
|
543
320
|
kind: "task_notification",
|
|
544
|
-
payload: JSON.stringify({ ...extras, task_id: n.task_id }),
|
|
321
|
+
payload: JSON.stringify({ ...extras, task_id: n.task_id }),
|
|
545
322
|
};
|
|
546
323
|
}
|
|
547
|
-
/**
|
|
548
|
-
* Resolve WHERE a workflow completion routes (core 1.208): the SESSION comes straight off
|
|
549
|
-
* `originatingSessionId` (the Runner closes over it at RunWorkflow mount — lookup-free). The OWNER comes from,
|
|
550
|
-
* in order:
|
|
551
|
-
* 1. the originating RUN ROW's owner (`getRun(sourceTaskId)`, 1.75) — immutable, stamped at submit. Only the
|
|
552
|
-
* BG leg has a run-row `sourceTaskId` though: the sync + resume legs deliberately leave `spec.taskId` unset
|
|
553
|
-
* (subagent nesting), so core derives `sourceTaskId = sessionId` and this lookup MISSES for them (review
|
|
554
|
-
* review, 1.78) — which under the old rule silently killed the multi-tenant push for every sync-leg workflow;
|
|
555
|
-
* 2. the payload's `principal` — core threads `spec.principal`, which in THIS service is ALWAYS the VERIFIED
|
|
556
|
-
* identity (the 1.55 F-fix invariant, main.ts: gated door = BFF-verified header, direct door = JWT `sub`,
|
|
557
|
-
* never the spoofable header; core 1.187 hardened the `TaskSpec.principal` contract to exactly this).
|
|
558
|
-
* Captured at SPAWN time → immutable, no delete-then-reclaim window. This is also the same value core's own
|
|
559
|
-
* workflow scope guard (`canAccessWorkflowRun`) keys off;
|
|
560
|
-
* 3. neither → single-user stamps `owner: null` (no tenancy boundary); multi-tenant (`requirePrincipal`)
|
|
561
|
-
* returns undefined = DROP fail-closed (an unverifiable owner must not ride the reclaim gate; the
|
|
562
|
-
* `WorkflowStatus` poll floor covers). Returns undefined when there is no route at all.
|
|
563
|
-
* The `getRun` lookup also carries the session FALLBACK for a crash-RECOVERY notify payload that lacks
|
|
564
|
-
* `originatingSessionId` — since core 1.210 the durable `WorkflowRun` records it and the recovery
|
|
565
|
-
* re-derivation threads it through (workflow-notify-journal), so this fallback covers only pre-1.210 rows
|
|
566
|
-
* (or a writer that omitted it), not the recovery path wholesale.
|
|
567
|
-
*/
|
|
568
324
|
export async function resolveCompletionRoute(p, getRun, requirePrincipal) {
|
|
569
325
|
let sessionId = p.originatingSessionId;
|
|
570
326
|
let owner;
|
|
@@ -573,31 +329,23 @@ export async function resolveCompletionRoute(p, getRun, requirePrincipal) {
|
|
|
573
329
|
const originating = await getRun(p.sourceTaskId);
|
|
574
330
|
if (originating) {
|
|
575
331
|
sessionId ??= originating.sessionId ?? undefined;
|
|
576
|
-
owner = originating.owner ?? null;
|
|
332
|
+
owner = originating.owner ?? null;
|
|
577
333
|
}
|
|
578
334
|
}
|
|
579
335
|
catch {
|
|
580
|
-
// lookup outage: fall through — the session may still route via the payload; owner resolves below
|
|
581
336
|
}
|
|
582
337
|
}
|
|
583
338
|
if (!sessionId)
|
|
584
|
-
return undefined;
|
|
339
|
+
return undefined;
|
|
585
340
|
if (owner === undefined && p.principal !== undefined)
|
|
586
|
-
owner = p.principal;
|
|
341
|
+
owner = p.principal;
|
|
587
342
|
if (owner === undefined) {
|
|
588
343
|
if (requirePrincipal)
|
|
589
|
-
return undefined;
|
|
590
|
-
owner = null;
|
|
344
|
+
return undefined;
|
|
345
|
+
owner = null;
|
|
591
346
|
}
|
|
592
347
|
return { sessionId, owner };
|
|
593
348
|
}
|
|
594
|
-
/**
|
|
595
|
-
* core 1.232 — resolve WHICH session's inbox entry a poll-served ack targets: the payload's
|
|
596
|
-
* `originatingSessionId` directly (core threads it on `ackServed`), else the originating run row via
|
|
597
|
-
* `sourceTaskId` (mirrors {@link resolveCompletionRoute}'s session half — same fallback, same reason:
|
|
598
|
-
* older runs may predate the lookup-free field). Returns undefined when there is no attribution — the
|
|
599
|
-
* entry stays pending (at-least-once; the shell's own dedup covers a rare duplicate).
|
|
600
|
-
*/
|
|
601
349
|
export async function resolveServedSession(p, getRun) {
|
|
602
350
|
if (p.originatingSessionId)
|
|
603
351
|
return p.originatingSessionId;
|
|
@@ -606,7 +354,6 @@ export async function resolveServedSession(p, getRun) {
|
|
|
606
354
|
return (await getRun(p.sourceTaskId))?.sessionId ?? undefined;
|
|
607
355
|
}
|
|
608
356
|
catch {
|
|
609
|
-
/* lookup outage → no attribution; the poll floor + shell dedup cover */
|
|
610
357
|
}
|
|
611
358
|
}
|
|
612
359
|
return undefined;
|