open-multi-agent-kit 1.2.2 → 1.2.4

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.
Files changed (84) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +2 -2
  3. package/dist/cli/help.d.ts.map +1 -1
  4. package/dist/cli/help.js +2 -0
  5. package/dist/cli/help.js.map +1 -1
  6. package/dist/commands/doctor-provider.d.ts.map +1 -1
  7. package/dist/commands/doctor-provider.js +2 -13
  8. package/dist/commands/doctor-provider.js.map +1 -1
  9. package/dist/commands/provider-adopt-cli.d.ts +24 -0
  10. package/dist/commands/provider-adopt-cli.d.ts.map +1 -0
  11. package/dist/commands/provider-adopt-cli.js +182 -0
  12. package/dist/commands/provider-adopt-cli.js.map +1 -0
  13. package/dist/commands/provider-adopt-status.d.ts +21 -0
  14. package/dist/commands/provider-adopt-status.d.ts.map +1 -0
  15. package/dist/commands/provider-adopt-status.js +84 -0
  16. package/dist/commands/provider-adopt-status.js.map +1 -0
  17. package/dist/commands/provider-known-apis.d.ts +2 -0
  18. package/dist/commands/provider-known-apis.d.ts.map +1 -0
  19. package/dist/commands/provider-known-apis.js +28 -0
  20. package/dist/commands/provider-known-apis.js.map +1 -0
  21. package/dist/commands/run-command.d.ts.map +1 -1
  22. package/dist/commands/run-command.js +4 -0
  23. package/dist/commands/run-command.js.map +1 -1
  24. package/dist/core/agent-session.d.ts.map +1 -1
  25. package/dist/core/agent-session.js +6 -0
  26. package/dist/core/agent-session.js.map +1 -1
  27. package/dist/core/auth-storage.d.ts +16 -16
  28. package/dist/core/auth-storage.d.ts.map +1 -1
  29. package/dist/core/auth-storage.js +94 -111
  30. package/dist/core/auth-storage.js.map +1 -1
  31. package/dist/core/compaction/compaction.d.ts.map +1 -1
  32. package/dist/core/compaction/compaction.js +13 -10
  33. package/dist/core/compaction/compaction.js.map +1 -1
  34. package/dist/core/compaction/inert-tail.d.ts +23 -0
  35. package/dist/core/compaction/inert-tail.d.ts.map +1 -0
  36. package/dist/core/compaction/inert-tail.js +108 -0
  37. package/dist/core/compaction/inert-tail.js.map +1 -0
  38. package/dist/core/compaction/provenance.d.ts +30 -0
  39. package/dist/core/compaction/provenance.d.ts.map +1 -0
  40. package/dist/core/compaction/provenance.js +59 -0
  41. package/dist/core/compaction/provenance.js.map +1 -0
  42. package/dist/core/compaction/transaction.d.ts.map +1 -1
  43. package/dist/core/compaction/transaction.js +4 -1
  44. package/dist/core/compaction/transaction.js.map +1 -1
  45. package/dist/core/external-credential-sources.d.ts +48 -0
  46. package/dist/core/external-credential-sources.d.ts.map +1 -0
  47. package/dist/core/external-credential-sources.js +176 -0
  48. package/dist/core/external-credential-sources.js.map +1 -0
  49. package/dist/core/model-registry.d.ts +11 -0
  50. package/dist/core/model-registry.d.ts.map +1 -1
  51. package/dist/core/model-registry.js +28 -14
  52. package/dist/core/model-registry.js.map +1 -1
  53. package/dist/core/oauth-account-import.d.ts +52 -0
  54. package/dist/core/oauth-account-import.d.ts.map +1 -0
  55. package/dist/core/oauth-account-import.js +125 -0
  56. package/dist/core/oauth-account-import.js.map +1 -0
  57. package/dist/core/oauth-credential.d.ts +22 -0
  58. package/dist/core/oauth-credential.d.ts.map +1 -0
  59. package/dist/core/oauth-credential.js +36 -0
  60. package/dist/core/oauth-credential.js.map +1 -0
  61. package/dist/core/session-compaction-service.d.ts +2 -2
  62. package/dist/core/session-compaction-service.d.ts.map +1 -1
  63. package/dist/core/session-compaction-service.js +20 -57
  64. package/dist/core/session-compaction-service.js.map +1 -1
  65. package/dist/core/verified-run/owned-execution.d.ts.map +1 -1
  66. package/dist/core/verified-run/owned-execution.js +13 -1
  67. package/dist/core/verified-run/owned-execution.js.map +1 -1
  68. package/dist/core/verified-run/supervisor-adapter.d.ts +4 -0
  69. package/dist/core/verified-run/supervisor-adapter.d.ts.map +1 -1
  70. package/dist/core/verified-run/supervisor-adapter.js +4 -0
  71. package/dist/core/verified-run/supervisor-adapter.js.map +1 -1
  72. package/docs/compaction.md +2 -0
  73. package/docs/extensions.md +1 -1
  74. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  75. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  76. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  77. package/examples/extensions/gondolin/package-lock.json +2 -2
  78. package/examples/extensions/gondolin/package.json +1 -1
  79. package/examples/extensions/sandbox/package-lock.json +2 -2
  80. package/examples/extensions/sandbox/package.json +1 -1
  81. package/examples/extensions/with-deps/package-lock.json +2 -2
  82. package/examples/extensions/with-deps/package.json +1 -1
  83. package/npm-shrinkwrap.json +18 -18
  84. package/package.json +6 -6
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-credential.js","sourceRoot":"","sources":["../../src/core/oauth-credential.ts"],"names":[],"mappings":"AAkBA,MAAM,UAAU,yBAAyB,CAAC,UAA2B,EAAoB;IACxF,MAAM,EACL,IAAI,EAAE,KAAK,EACX,QAAQ,EAAE,SAAS,EACnB,aAAa,EAAE,cAAc,EAC7B,WAAW,EAAE,YAAY,EACzB,GAAG,WAAW,EACd,GAAG,UAAU,CAAC;IACf,OAAO,WAAW,CAAC;AAAA,CACnB;AAED,MAAM,UAAU,gBAAgB,CAAC,UAA2B,EAAsB;IACjF,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1E,OAAO,UAAU,CAAC,QAAQ,CAAC;IAC5B,CAAC;IACD,OAAO,CAAC,yBAAyB,CAAC,UAAU,CAAC,CAAC,CAAC;AAAA,CAC/C;AAED,MAAM,UAAU,0BAA0B,CAAC,KAAc,EAAE,YAAoB,EAAU;IACxF,IAAI,YAAY,IAAI,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QAChF,OAAO,CAAC,CAAC;IACV,CAAC;IACD,OAAO,CAAC,CAAC,KAAK,GAAG,YAAY,CAAC,GAAG,YAAY,CAAC,GAAG,YAAY,CAAC;AAAA,CAC9D;AAED,MAAM,UAAU,4BAA4B,CAAC,UAA2B,EAAE,YAAoB,EAAU;IACvG,OAAO,0BAA0B,CAAC,UAAU,CAAC,aAAa,IAAI,UAAU,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;AAAA,CACpG;AAED,MAAM,UAAU,qBAAqB,CAAC,QAA4B,EAAE,aAAa,GAAG,CAAC,EAAmB;IACvG,MAAM,aAAa,GAAG,0BAA0B,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjF,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;IACzC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,GAAG,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IACvC,CAAC;IACD,OAAO;QACN,GAAG,QAAQ;QACX,IAAI,EAAE,OAAO;QACb,QAAQ;QACR,aAAa,EAAE,aAAa;KAC5B,CAAC;AAAA,CACF","sourcesContent":["/**\n * Pure helpers for the stored `oauth` credential shape: account lists, the\n * selected-account index, and building a credential back from an account list.\n * Extracted from auth-storage.ts so the store keeps its size budget; nothing here\n * touches the storage lock or disk.\n */\nimport type { OAuthCredentials } from \"omk-ai\";\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n\t/** Complete account list when more than one subscription account is configured. */\n\taccounts?: OAuthCredentials[];\n\t/** Index of the account explicitly selected for this provider. */\n\tactiveAccount?: number;\n\t/** Legacy cursor accepted when migrating older auth.json files. */\n\tnextAccount?: number;\n} & OAuthCredentials;\n\nexport function stripOAuthStorageMetadata(credential: OAuthCredential): OAuthCredentials {\n\tconst {\n\t\ttype: _type,\n\t\taccounts: _accounts,\n\t\tactiveAccount: _activeAccount,\n\t\tnextAccount: _nextAccount,\n\t\t...credentials\n\t} = credential;\n\treturn credentials;\n}\n\nexport function getOAuthAccounts(credential: OAuthCredential): OAuthCredentials[] {\n\tif (Array.isArray(credential.accounts) && credential.accounts.length > 0) {\n\t\treturn credential.accounts;\n\t}\n\treturn [stripOAuthStorageMetadata(credential)];\n}\n\nexport function normalizeOAuthAccountIndex(index: unknown, accountCount: number): number {\n\tif (accountCount <= 0 || typeof index !== \"number\" || !Number.isInteger(index)) {\n\t\treturn 0;\n\t}\n\treturn ((index % accountCount) + accountCount) % accountCount;\n}\n\nexport function getSelectedOAuthAccountIndex(credential: OAuthCredential, accountCount: number): number {\n\treturn normalizeOAuthAccountIndex(credential.activeAccount ?? credential.nextAccount, accountCount);\n}\n\nexport function createOAuthCredential(accounts: OAuthCredentials[], activeAccount = 0): OAuthCredential {\n\tconst selectedIndex = normalizeOAuthAccountIndex(activeAccount, accounts.length);\n\tconst selected = accounts[selectedIndex];\n\tif (!selected) {\n\t\tthrow new Error(\"OAuth credential must contain at least one account\");\n\t}\n\tif (accounts.length === 1) {\n\t\treturn { ...selected, type: \"oauth\" };\n\t}\n\treturn {\n\t\t...selected,\n\t\ttype: \"oauth\",\n\t\taccounts,\n\t\tactiveAccount: selectedIndex,\n\t};\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import type { Api, Message, Model } from "omk-ai";
2
2
  import { type CompactionControlState } from "./compaction/control-state.ts";
3
3
  import type { CompactionResult } from "./compaction/index.ts";
4
- import { type CompactionBarrierResult, type CompactionEnvelope, type CompactionPreservedProvenanceInput, type CompactionSourceIdentity, type CompactionTransaction } from "./compaction/transaction.ts";
4
+ import { type CompactionBarrierResult, type CompactionEnvelope, type CompactionSourceIdentity, type CompactionTransaction } from "./compaction/transaction.ts";
5
5
  import type { SessionIntegrityReport } from "./session-integrity.ts";
6
6
  import type { CompactionEntry, SessionEntry, SessionManager } from "./session-manager.ts";
7
7
  export interface CapturedCompactionState {
@@ -47,6 +47,7 @@ export declare class SessionCompactionService {
47
47
  private readonly deps;
48
48
  constructor(deps: SessionCompactionServiceDeps);
49
49
  captureState(): CapturedCompactionState;
50
+ private readSessionBytes;
50
51
  private captureStateLocked;
51
52
  barrierError(barrier: CompactionBarrierResult): Error;
52
53
  evaluateBarrier(capture: CapturedCompactionState, includeMissingTailAsPending: boolean, excludedPendingIds?: ReadonlySet<string>): CompactionBarrierResult;
@@ -56,7 +57,6 @@ export declare class SessionCompactionService {
56
57
  };
57
58
  priorCommittedSourceDigests(): string[];
58
59
  beginTransaction(compactionModel: Model<Api>, emergency: boolean): BegunCompaction;
59
- captureProvenance(capture: CapturedCompactionState): CompactionPreservedProvenanceInput;
60
60
  detailsWithEnvelope(details: unknown, envelope: CompactionEnvelope): unknown;
61
61
  commit(begun: BegunCompaction, result: CompactionResult, fromExtension: boolean): CommittedCompaction;
62
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session-compaction-service.d.ts","sourceRoot":"","sources":["../../src/core/session-compaction-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session compaction service: capture / barrier / repair / transaction / commit.\n *\n * Extracted from AgentSession so the session keeps lifecycle orchestration\n * (abort controllers, extension events, auth, model resolution) while this\n * owns the integrity-critical compaction state machine. Every method preserves\n * the exact semantics of the original session implementation — the\n * compaction-transaction / session-integrity suites pin them.\n */\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { repairTranscriptIntegrity } from \"omk-agent-core\";\nimport type { Api, Message, Model, ToolResultMessage } from \"omk-ai\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport {\n\ttype CompactionControlState,\n\tcontrolStateDigest,\n\tcontrolStateProvenance,\n\tvalidateControlState,\n} from \"./compaction/control-state.ts\";\nimport type { CompactionResult } from \"./compaction/index.ts\";\nimport {\n\ttype CompactionBarrierResult,\n\ttype CompactionEnvelope,\n\ttype CompactionPreservedProvenanceInput,\n\ttype CompactionSourceIdentity,\n\ttype CompactionTransaction,\n\tcreateCompactionEnvelope,\n\tcreateCompactionSourceIdentity,\n\tcreateCompactionTransaction,\n\tdecideCompactionCommit,\n\tevaluateCompactionBarrier,\n\tredactCredentialShapedContent,\n\tvalidateCompactionEnvelope,\n} from \"./compaction/transaction.ts\";\nimport { redactSensitiveTextForced } from \"./redaction.ts\";\nimport type { SessionIntegrityReport } from \"./session-integrity.ts\";\nimport { inspectSessionIntegrity } from \"./session-integrity.ts\";\nimport type { CompactionEntry, SessionEntry, SessionManager } from \"./session-manager.ts\";\n\nexport interface CapturedCompactionState {\n\treadonly report: SessionIntegrityReport;\n\treadonly branchEntries: readonly SessionEntry[];\n\treadonly revision: CompactionTransaction[\"baseRevision\"];\n\treadonly source: CompactionSourceIdentity;\n\t/**\n\t * Digest of the host control-state snapshot captured under the same commit\n\t * lock as `source`. `null` when no control authority is attached. A change\n\t * between capture and commit discards the summary like a source mismatch.\n\t */\n\treadonly controlStateDigest: string | null;\n\treadonly controlState: CompactionControlState | null;\n}\n\nexport interface BegunCompaction {\n\treadonly capture: CapturedCompactionState;\n\treadonly transaction: CompactionTransaction;\n}\n\nexport interface CommittedCompaction {\n\treadonly entry: CompactionEntry;\n\treadonly envelope: CompactionEnvelope;\n}\n\nexport interface SessionCompactionServiceDeps {\n\treadonly sessionManager: SessionManager;\n\treadonly pendingToolCallIds: () => ReadonlySet<string>;\n\treadonly getUserMessageText: (message: Message) => string;\n\treadonly cwd: string;\n\t/**\n\t * Optional host control authority. Called inside the compaction commit lock\n\t * so the snapshot is bound to the same captured source as the summary. Must\n\t * be deterministic for unchanged state — a changed snapshot discards the\n\t * in-flight summary at commit. Return `null` only when no authority exists.\n\t */\n\treadonly controlState?: () => CompactionControlState | null;\n\treadonly invalidateContextBudget: () => void;\n\t/** Refresh agent messages from the session manager after tail repair/commit. */\n\treadonly refreshAgentMessages: () => void;\n\t/** Hysteresis bookkeeping after a successful commit. */\n\treadonly recordCommit: () => void;\n}\n\nexport class SessionCompactionService {\n\tprivate readonly deps: SessionCompactionServiceDeps;\n\n\tconstructor(deps: SessionCompactionServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tcaptureState(): CapturedCompactionState {\n\t\treturn this.deps.sessionManager.withCompactionCommitLock(() => this.captureStateLocked());\n\t}\n\n\tprivate captureStateLocked(): CapturedCompactionState {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst sessionFile = sessionManager.getSessionFile();\n\t\tconst bytes =\n\t\t\tsessionFile && existsSync(sessionFile)\n\t\t\t\t? new Uint8Array(readFileSync(sessionFile))\n\t\t\t\t: new TextEncoder().encode(\n\t\t\t\t\t\t`${[sessionManager.getHeader(), ...sessionManager.getEntries()]\n\t\t\t\t\t\t\t.filter((entry) => entry !== null)\n\t\t\t\t\t\t\t.map((entry) => JSON.stringify(entry))\n\t\t\t\t\t\t\t.join(\"\\n\")}\\n`,\n\t\t\t\t\t);\n\t\tconst report = inspectSessionIntegrity(bytes, { activeLeafId: sessionManager.getLeafId() });\n\t\tconst branchEntries = report.activeBranch;\n\t\tlet latestCompactionIndex = -1;\n\t\tfor (let index = branchEntries.length - 1; index >= 0; index -= 1) {\n\t\t\tif (branchEntries[index]?.type === \"compaction\") {\n\t\t\t\tlatestCompactionIndex = index;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tconst latestCompaction = branchEntries[latestCompactionIndex];\n\t\tconst firstKeptIndex =\n\t\t\tlatestCompaction?.type === \"compaction\"\n\t\t\t\t? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)\n\t\t\t\t: -1;\n\t\tconst sourceEntries = branchEntries.slice(\n\t\t\tlatestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex,\n\t\t);\n\t\tconst firstEntry = sourceEntries[0];\n\t\tconst lastEntry = sourceEntries[sourceEntries.length - 1];\n\t\tif (!firstEntry || !lastEntry || report.activeLeafId === null) {\n\t\t\tconst barrier = evaluateCompactionBarrier(report, [...this.deps.pendingToolCallIds()]);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\tthrow new Error(\"Nothing to compact: the active session branch is empty\");\n\t\t}\n\t\tconst revision = sessionManager.getDurableHeadToken();\n\t\tconst source = createCompactionSourceIdentity({\n\t\t\tsessionId: revision.sessionId,\n\t\t\tentryIds: sourceEntries.map((entry) => entry.id),\n\t\t\tfirstEntryId: firstEntry.id,\n\t\t\tlastEntryId: lastEntry.id,\n\t\t\tsourceSha256: createHash(\"sha256\")\n\t\t\t\t.update(sourceEntries.map((entry) => JSON.stringify(entry)).join(\"\\n\"), \"utf8\")\n\t\t\t\t.digest(\"hex\"),\n\t\t\tactiveLeafId: report.activeLeafId,\n\t\t\tmessageCount: report.activeMessages.length,\n\t\t});\n\t\tconst controlState = validateControlState(this.deps.controlState?.() ?? null);\n\t\treturn {\n\t\t\treport,\n\t\t\tbranchEntries,\n\t\t\trevision,\n\t\t\tsource,\n\t\t\tcontrolStateDigest: controlStateDigest(controlState),\n\t\t\tcontrolState,\n\t\t};\n\t}\n\n\tbarrierError(barrier: CompactionBarrierResult): Error {\n\t\tif (barrier.status === \"defer\") {\n\t\t\treturn new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);\n\t\t}\n\t\treturn new Error(\n\t\t\t`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`,\n\t\t);\n\t}\n\n\tevaluateBarrier(\n\t\tcapture: CapturedCompactionState,\n\t\tincludeMissingTailAsPending: boolean,\n\t\texcludedPendingIds: ReadonlySet<string> = new Set(),\n\t): CompactionBarrierResult {\n\t\tconst pending = new Set([...this.deps.pendingToolCallIds()].filter((id) => !excludedPendingIds.has(id)));\n\t\tif (includeMissingTailAsPending) {\n\t\t\tfor (const issue of capture.report.transcript?.issues ?? []) {\n\t\t\t\tif (issue.kind === \"missing_result\") pending.add(issue.toolCallId);\n\t\t\t}\n\t\t}\n\t\treturn evaluateCompactionBarrier(capture.report, [...pending]);\n\t}\n\n\trepairEmergencyTail(capture: CapturedCompactionState): {\n\t\treadonly capture: CapturedCompactionState;\n\t\treadonly repairedToolCallIds: ReadonlySet<string>;\n\t} {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst barrier = this.evaluateBarrier(capture, true);\n\t\tif (barrier.status !== \"defer\" || barrier.reason !== \"missing_active_tail_results\") {\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\treturn { capture, repairedToolCallIds: new Set() };\n\t\t}\n\t\tconst repairedMessages = repairTranscriptIntegrity(\n\t\t\t[...capture.report.activeMessages],\n\t\t\t\"Tool result missing; synthesized to close an emergency compaction barrier\",\n\t\t);\n\t\tconst inserted = repairedMessages.slice(capture.report.activeMessages.length);\n\t\tconst repairedToolCallIds = new Set<string>();\n\t\tfor (const message of inserted) {\n\t\t\tif (message.role !== \"toolResult\") {\n\t\t\t\tthrow new Error(\"Emergency compaction repair produced a non-tool result\");\n\t\t\t}\n\t\t\tconst toolResult: ToolResultMessage = message;\n\t\t\trepairedToolCallIds.add(toolResult.toolCallId);\n\t\t\tsessionManager.appendMessage(toolResult);\n\t\t}\n\t\tsessionManager.appendCustomEntry(\"compaction_transcript_repaired\", {\n\t\t\tinsertedToolCallIds: [...repairedToolCallIds],\n\t\t\treason: \"emergency_compaction\",\n\t\t});\n\t\tthis.deps.invalidateContextBudget();\n\t\tconst closedCapture = this.captureState();\n\t\tconst closedBarrier = this.evaluateBarrier(closedCapture, false, repairedToolCallIds);\n\t\tif (closedBarrier.status !== \"ready\") throw this.barrierError(closedBarrier);\n\t\tthis.deps.refreshAgentMessages();\n\t\treturn { capture: closedCapture, repairedToolCallIds };\n\t}\n\n\tpriorCommittedSourceDigests(): string[] {\n\t\tconst digests: string[] = [];\n\t\tfor (const entry of this.deps.sessionManager.getEntries()) {\n\t\t\tif (entry.type !== \"compaction\" || typeof entry.details !== \"object\" || entry.details === null) continue;\n\t\t\tif (Object.getOwnPropertyDescriptor(entry.details, \"compactionEnvelope\") === undefined) continue;\n\t\t\tconst envelope = validateCompactionEnvelope(Reflect.get(entry.details, \"compactionEnvelope\"));\n\t\t\tif (envelope.summary !== entry.summary) {\n\t\t\t\tthrow new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);\n\t\t\t}\n\t\t\tdigests.push(envelope.source.sourceSha256);\n\t\t}\n\t\treturn digests;\n\t}\n\n\tbeginTransaction(compactionModel: Model<Api>, emergency: boolean): BegunCompaction {\n\t\tlet capture = this.captureState();\n\t\tif (emergency) {\n\t\t\tcapture = this.repairEmergencyTail(capture).capture;\n\t\t} else {\n\t\t\tconst barrier = this.evaluateBarrier(capture, false);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t}\n\t\tconst transaction = createCompactionTransaction({\n\t\t\ttransactionId: randomUUID(),\n\t\t\tbaseRevision: capture.revision,\n\t\t\tsource: capture.source,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t\tmodel: { provider: compactionModel.provider, id: compactionModel.id },\n\t\t\tpreserved: this.captureProvenance(capture),\n\t\t});\n\t\tif (this.priorCommittedSourceDigests().includes(transaction.source.sourceSha256)) {\n\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t}\n\t\treturn { capture, transaction };\n\t}\n\n\tcaptureProvenance(capture: CapturedCompactionState): CompactionPreservedProvenanceInput {\n\t\tlet latestIntent = \"Continue the current session\";\n\t\tfor (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {\n\t\t\tconst message = capture.report.activeMessages[index];\n\t\t\tif (message?.role !== \"user\") continue;\n\t\t\tconst candidate = redactCredentialShapedContent(\n\t\t\t\tsanitizeBinaryOutput(redactSensitiveTextForced(this.deps.getUserMessageText(message)).trim())\n\t\t\t\t\t.replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n\t\t\t\t\t.slice(0, 16_384),\n\t\t\t);\n\t\t\tif (candidate.length > 0) latestIntent = candidate;\n\t\t\tbreak;\n\t\t}\n\t\tconst modelHistory = capture.branchEntries\n\t\t\t.flatMap((entry) => {\n\t\t\t\tif (entry.type === \"model_change\") {\n\t\t\t\t\treturn [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];\n\t\t\t\t}\n\t\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\t\treturn [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];\n\t\t\t\t}\n\t\t\t\treturn [];\n\t\t\t})\n\t\t\t.slice(-256);\n\t\tconst customEntryIds = (customType: string): string[] =>\n\t\t\tcapture.branchEntries\n\t\t\t\t.filter((entry) => entry.type === \"custom\" && entry.customType === customType)\n\t\t\t\t.map((entry) => entry.id);\n\t\tconst control = controlStateProvenance(capture.controlState);\n\t\treturn {\n\t\t\tlatestIntent,\n\t\t\topenTasks: control.openTasks,\n\t\t\tlaneIds: customEntryIds(\"lane\"),\n\t\t\tacceptancePredicateIds: customEntryIds(\"acceptance_predicate\"),\n\t\t\tevidenceReceiptIds: customEntryIds(\"evidence_receipt\"),\n\t\t\tblockerReasons: control.blockerReasons,\n\t\t\trepairEventIds: [\n\t\t\t\t...customEntryIds(\"transcript_repaired\"),\n\t\t\t\t...customEntryIds(\"compaction_transcript_repaired\"),\n\t\t\t],\n\t\t\tbranch: control.branch,\n\t\t\tworktree: this.deps.cwd,\n\t\t\tmodelHistory,\n\t\t\tnextAction: redactCredentialShapedContent(latestIntent.slice(0, 4096)) || \"Continue the current session\",\n\t\t};\n\t}\n\n\tdetailsWithEnvelope(details: unknown, envelope: CompactionEnvelope): unknown {\n\t\tif (typeof details === \"object\" && details !== null && !Array.isArray(details)) {\n\t\t\treturn { ...details, compactionEnvelope: envelope };\n\t\t}\n\t\treturn {\n\t\t\tcompactionEnvelope: envelope,\n\t\t\t...(details === undefined ? {} : { resultDetails: details }),\n\t\t};\n\t}\n\n\tcommit(begun: BegunCompaction, result: CompactionResult, fromExtension: boolean): CommittedCompaction {\n\t\tif (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {\n\t\t\tthrow new Error(\"Compaction first-kept entry is outside the captured source\");\n\t\t}\n\t\tconst committed = this.deps.sessionManager.withCompactionCommitLock(() => {\n\t\t\tconst current = this.captureState();\n\t\t\tconst barrier = this.evaluateBarrier(current, false);\n\t\t\tconst decision = decideCompactionCommit({\n\t\t\t\ttransaction: begun.transaction,\n\t\t\t\tcurrentRevision: current.revision,\n\t\t\t\tcurrentSource: current.source,\n\t\t\t\tbarrier,\n\t\t\t\tpriorCommittedSourceDigests: this.priorCommittedSourceDigests(),\n\t\t\t});\n\t\t\tswitch (decision.decision) {\n\t\t\t\tcase \"duplicate\":\n\t\t\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t\t\tcase \"stale\":\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Session changed during compaction (${decision.reason}); generated summary was discarded`,\n\t\t\t\t\t);\n\t\t\t\tcase \"defer\":\n\t\t\t\tcase \"fail_closed\":\n\t\t\t\t\tthrow this.barrierError(barrier);\n\t\t\t\tcase \"commit\": {\n\t\t\t\t\tif (current.controlStateDigest !== begun.capture.controlStateDigest) {\n\t\t\t\t\t\tthrow new Error(\"Control state changed during compaction; generated summary was discarded\");\n\t\t\t\t\t}\n\t\t\t\t\tconst envelope = createCompactionEnvelope({\n\t\t\t\t\t\ttransaction: begun.transaction,\n\t\t\t\t\t\tdecision,\n\t\t\t\t\t\tsummary: result.summary,\n\t\t\t\t\t\tsummarySha256: createHash(\"sha256\").update(result.summary, \"utf8\").digest(\"hex\"),\n\t\t\t\t\t});\n\t\t\t\t\tconst entryId = this.deps.sessionManager.appendCompaction(\n\t\t\t\t\t\tresult.summary,\n\t\t\t\t\t\tresult.firstKeptEntryId,\n\t\t\t\t\t\tresult.tokensBefore,\n\t\t\t\t\t\tthis.detailsWithEnvelope(result.details, envelope),\n\t\t\t\t\t\tfromExtension,\n\t\t\t\t\t);\n\t\t\t\t\tconst entry = this.deps.sessionManager.getEntry(entryId);\n\t\t\t\t\tif (!entry || entry.type !== \"compaction\") {\n\t\t\t\t\t\tthrow new Error(\"Compaction commit did not produce a compaction entry\");\n\t\t\t\t\t}\n\t\t\t\t\treturn { entry, envelope };\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis.deps.recordCommit();\n\t\treturn committed;\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-compaction-service.d.ts","sourceRoot":"","sources":["../../src/core/session-compaction-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session compaction service: capture / barrier / repair / transaction / commit.\n *\n * Extracted from AgentSession so the session keeps lifecycle orchestration\n * (abort controllers, extension events, auth, model resolution) while this\n * owns the integrity-critical compaction state machine. Every method preserves\n * the exact semantics of the original session implementation — the\n * compaction-transaction / session-integrity suites pin them.\n */\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { repairTranscriptIntegrity } from \"omk-agent-core\";\nimport type { Api, Message, Model, ToolResultMessage } from \"omk-ai\";\nimport { type CompactionControlState, controlStateDigest, validateControlState } from \"./compaction/control-state.ts\";\nimport type { CompactionResult } from \"./compaction/index.ts\";\nimport { decideCommitOverInertTail } from \"./compaction/inert-tail.ts\";\nimport { buildPreservedProvenance, PROVENANCE_CUSTOM_TYPES } from \"./compaction/provenance.ts\";\nimport {\n\ttype CompactionBarrierResult,\n\ttype CompactionEnvelope,\n\ttype CompactionSourceIdentity,\n\ttype CompactionTransaction,\n\tcreateCompactionEnvelope,\n\tcreateCompactionSourceIdentity,\n\tcreateCompactionTransaction,\n\tevaluateCompactionBarrier,\n\tvalidateCompactionEnvelope,\n} from \"./compaction/transaction.ts\";\nimport type { SessionIntegrityReport } from \"./session-integrity.ts\";\nimport { inspectSessionIntegrity } from \"./session-integrity.ts\";\nimport type { CompactionEntry, SessionEntry, SessionManager } from \"./session-manager.ts\";\n\nexport interface CapturedCompactionState {\n\treadonly report: SessionIntegrityReport;\n\treadonly branchEntries: readonly SessionEntry[];\n\treadonly revision: CompactionTransaction[\"baseRevision\"];\n\treadonly source: CompactionSourceIdentity;\n\t/**\n\t * Digest of the host control-state snapshot captured under the same commit\n\t * lock as `source`. `null` when no control authority is attached. A change\n\t * between capture and commit discards the summary like a source mismatch.\n\t */\n\treadonly controlStateDigest: string | null;\n\treadonly controlState: CompactionControlState | null;\n}\n\nexport interface BegunCompaction {\n\treadonly capture: CapturedCompactionState;\n\treadonly transaction: CompactionTransaction;\n}\n\nexport interface CommittedCompaction {\n\treadonly entry: CompactionEntry;\n\treadonly envelope: CompactionEnvelope;\n}\n\nexport interface SessionCompactionServiceDeps {\n\treadonly sessionManager: SessionManager;\n\treadonly pendingToolCallIds: () => ReadonlySet<string>;\n\treadonly getUserMessageText: (message: Message) => string;\n\treadonly cwd: string;\n\t/**\n\t * Optional host control authority. Called inside the compaction commit lock\n\t * so the snapshot is bound to the same captured source as the summary. Must\n\t * be deterministic for unchanged state — a changed snapshot discards the\n\t * in-flight summary at commit. Return `null` only when no authority exists.\n\t */\n\treadonly controlState?: () => CompactionControlState | null;\n\treadonly invalidateContextBudget: () => void;\n\t/** Refresh agent messages from the session manager after tail repair/commit. */\n\treadonly refreshAgentMessages: () => void;\n\t/** Hysteresis bookkeeping after a successful commit. */\n\treadonly recordCommit: () => void;\n}\n\nexport class SessionCompactionService {\n\tprivate readonly deps: SessionCompactionServiceDeps;\n\n\tconstructor(deps: SessionCompactionServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tcaptureState(): CapturedCompactionState {\n\t\treturn this.deps.sessionManager.withCompactionCommitLock(() => this.captureStateLocked(this.readSessionBytes()));\n\t}\n\n\tprivate readSessionBytes(): Uint8Array {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst sessionFile = sessionManager.getSessionFile();\n\t\treturn sessionFile && existsSync(sessionFile)\n\t\t\t? new Uint8Array(readFileSync(sessionFile))\n\t\t\t: new TextEncoder().encode(\n\t\t\t\t\t`${[sessionManager.getHeader(), ...sessionManager.getEntries()]\n\t\t\t\t\t\t.filter((entry) => entry !== null)\n\t\t\t\t\t\t.map((entry) => JSON.stringify(entry))\n\t\t\t\t\t\t.join(\"\\n\")}\\n`,\n\t\t\t\t);\n\t}\n\n\tprivate captureStateLocked(bytes: Uint8Array): CapturedCompactionState {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst report = inspectSessionIntegrity(bytes, { activeLeafId: sessionManager.getLeafId() });\n\t\tconst branchEntries = report.activeBranch;\n\t\tlet latestCompactionIndex = -1;\n\t\tfor (let index = branchEntries.length - 1; index >= 0; index -= 1) {\n\t\t\tif (branchEntries[index]?.type === \"compaction\") {\n\t\t\t\tlatestCompactionIndex = index;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tconst latestCompaction = branchEntries[latestCompactionIndex];\n\t\tconst firstKeptIndex =\n\t\t\tlatestCompaction?.type === \"compaction\"\n\t\t\t\t? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)\n\t\t\t\t: -1;\n\t\tconst sourceEntries = branchEntries.slice(\n\t\t\tlatestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex,\n\t\t);\n\t\tconst firstEntry = sourceEntries[0];\n\t\tconst lastEntry = sourceEntries[sourceEntries.length - 1];\n\t\tif (!firstEntry || !lastEntry || report.activeLeafId === null) {\n\t\t\tconst barrier = evaluateCompactionBarrier(report, [...this.deps.pendingToolCallIds()]);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\tthrow new Error(\"Nothing to compact: the active session branch is empty\");\n\t\t}\n\t\tconst revision = sessionManager.getDurableHeadToken();\n\t\tconst source = createCompactionSourceIdentity({\n\t\t\tsessionId: revision.sessionId,\n\t\t\tentryIds: sourceEntries.map((entry) => entry.id),\n\t\t\tfirstEntryId: firstEntry.id,\n\t\t\tlastEntryId: lastEntry.id,\n\t\t\tsourceSha256: createHash(\"sha256\")\n\t\t\t\t.update(sourceEntries.map((entry) => JSON.stringify(entry)).join(\"\\n\"), \"utf8\")\n\t\t\t\t.digest(\"hex\"),\n\t\t\tactiveLeafId: report.activeLeafId,\n\t\t\tmessageCount: report.activeMessages.length,\n\t\t});\n\t\tconst controlState = validateControlState(this.deps.controlState?.() ?? null);\n\t\treturn {\n\t\t\treport,\n\t\t\tbranchEntries,\n\t\t\trevision,\n\t\t\tsource,\n\t\t\tcontrolStateDigest: controlStateDigest(controlState),\n\t\t\tcontrolState,\n\t\t};\n\t}\n\n\tbarrierError(barrier: CompactionBarrierResult): Error {\n\t\tif (barrier.status === \"defer\") {\n\t\t\treturn new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);\n\t\t}\n\t\treturn new Error(\n\t\t\t`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`,\n\t\t);\n\t}\n\n\tevaluateBarrier(\n\t\tcapture: CapturedCompactionState,\n\t\tincludeMissingTailAsPending: boolean,\n\t\texcludedPendingIds: ReadonlySet<string> = new Set(),\n\t): CompactionBarrierResult {\n\t\tconst pending = new Set([...this.deps.pendingToolCallIds()].filter((id) => !excludedPendingIds.has(id)));\n\t\tif (includeMissingTailAsPending) {\n\t\t\tfor (const issue of capture.report.transcript?.issues ?? []) {\n\t\t\t\tif (issue.kind === \"missing_result\") pending.add(issue.toolCallId);\n\t\t\t}\n\t\t}\n\t\treturn evaluateCompactionBarrier(capture.report, [...pending]);\n\t}\n\n\trepairEmergencyTail(capture: CapturedCompactionState): {\n\t\treadonly capture: CapturedCompactionState;\n\t\treadonly repairedToolCallIds: ReadonlySet<string>;\n\t} {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst barrier = this.evaluateBarrier(capture, true);\n\t\tif (barrier.status !== \"defer\" || barrier.reason !== \"missing_active_tail_results\") {\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\treturn { capture, repairedToolCallIds: new Set() };\n\t\t}\n\t\tconst repairedMessages = repairTranscriptIntegrity(\n\t\t\t[...capture.report.activeMessages],\n\t\t\t\"Tool result missing; synthesized to close an emergency compaction barrier\",\n\t\t);\n\t\tconst inserted = repairedMessages.slice(capture.report.activeMessages.length);\n\t\tconst repairedToolCallIds = new Set<string>();\n\t\tfor (const message of inserted) {\n\t\t\tif (message.role !== \"toolResult\") {\n\t\t\t\tthrow new Error(\"Emergency compaction repair produced a non-tool result\");\n\t\t\t}\n\t\t\tconst toolResult: ToolResultMessage = message;\n\t\t\trepairedToolCallIds.add(toolResult.toolCallId);\n\t\t\tsessionManager.appendMessage(toolResult);\n\t\t}\n\t\tsessionManager.appendCustomEntry(PROVENANCE_CUSTOM_TYPES.compactionTranscriptRepaired, {\n\t\t\tinsertedToolCallIds: [...repairedToolCallIds],\n\t\t\treason: \"emergency_compaction\",\n\t\t});\n\t\tthis.deps.invalidateContextBudget();\n\t\tconst closedCapture = this.captureState();\n\t\tconst closedBarrier = this.evaluateBarrier(closedCapture, false, repairedToolCallIds);\n\t\tif (closedBarrier.status !== \"ready\") throw this.barrierError(closedBarrier);\n\t\tthis.deps.refreshAgentMessages();\n\t\treturn { capture: closedCapture, repairedToolCallIds };\n\t}\n\n\tpriorCommittedSourceDigests(): string[] {\n\t\tconst digests: string[] = [];\n\t\tfor (const entry of this.deps.sessionManager.getEntries()) {\n\t\t\tif (entry.type !== \"compaction\" || typeof entry.details !== \"object\" || entry.details === null) continue;\n\t\t\tif (Object.getOwnPropertyDescriptor(entry.details, \"compactionEnvelope\") === undefined) continue;\n\t\t\tconst envelope = validateCompactionEnvelope(Reflect.get(entry.details, \"compactionEnvelope\"));\n\t\t\tif (envelope.summary !== entry.summary) {\n\t\t\t\tthrow new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);\n\t\t\t}\n\t\t\tdigests.push(envelope.source.sourceSha256);\n\t\t}\n\t\treturn digests;\n\t}\n\n\tbeginTransaction(compactionModel: Model<Api>, emergency: boolean): BegunCompaction {\n\t\tlet capture = this.captureState();\n\t\tif (emergency) {\n\t\t\tcapture = this.repairEmergencyTail(capture).capture;\n\t\t} else {\n\t\t\tconst barrier = this.evaluateBarrier(capture, false);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t}\n\t\tconst transaction = createCompactionTransaction({\n\t\t\ttransactionId: randomUUID(),\n\t\t\tbaseRevision: capture.revision,\n\t\t\tsource: capture.source,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t\tmodel: { provider: compactionModel.provider, id: compactionModel.id },\n\t\t\tpreserved: buildPreservedProvenance(capture, this.deps.getUserMessageText, this.deps.cwd),\n\t\t});\n\t\tif (this.priorCommittedSourceDigests().includes(transaction.source.sourceSha256)) {\n\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t}\n\t\treturn { capture, transaction };\n\t}\n\n\tdetailsWithEnvelope(details: unknown, envelope: CompactionEnvelope): unknown {\n\t\tif (typeof details === \"object\" && details !== null && !Array.isArray(details)) {\n\t\t\treturn { ...details, compactionEnvelope: envelope };\n\t\t}\n\t\treturn {\n\t\t\tcompactionEnvelope: envelope,\n\t\t\t...(details === undefined ? {} : { resultDetails: details }),\n\t\t};\n\t}\n\n\tcommit(begun: BegunCompaction, result: CompactionResult, fromExtension: boolean): CommittedCompaction {\n\t\tif (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {\n\t\t\tthrow new Error(\"Compaction first-kept entry is outside the captured source\");\n\t\t}\n\t\tconst committed = this.deps.sessionManager.withCompactionCommitLock(() => {\n\t\t\tconst bytes = this.readSessionBytes();\n\t\t\tconst current = this.captureStateLocked(bytes);\n\t\t\tconst barrier = this.evaluateBarrier(current, false);\n\t\t\tconst { transaction, decision } = decideCommitOverInertTail({\n\t\t\t\ttransaction: begun.transaction,\n\t\t\t\tcurrentRevision: current.revision,\n\t\t\t\tcurrentSource: current.source,\n\t\t\t\tcurrentBytes: bytes,\n\t\t\t\tbarrier,\n\t\t\t\tpriorCommittedSourceDigests: this.priorCommittedSourceDigests(),\n\t\t\t\t// Extension state written during summarization must not livelock compaction,\n\t\t\t\t// but an extension-provided summary may read that state: never rebase it.\n\t\t\t\trebaseAllowed: !fromExtension,\n\t\t\t});\n\t\t\tswitch (decision.decision) {\n\t\t\t\tcase \"duplicate\":\n\t\t\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t\t\tcase \"stale\":\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Session changed during compaction (${decision.reason}); generated summary was discarded`,\n\t\t\t\t\t);\n\t\t\t\tcase \"defer\":\n\t\t\t\tcase \"fail_closed\":\n\t\t\t\t\tthrow this.barrierError(barrier);\n\t\t\t\tcase \"commit\": {\n\t\t\t\t\tif (current.controlStateDigest !== begun.capture.controlStateDigest) {\n\t\t\t\t\t\tthrow new Error(\"Control state changed during compaction; generated summary was discarded\");\n\t\t\t\t\t}\n\t\t\t\t\tconst envelope = createCompactionEnvelope({\n\t\t\t\t\t\ttransaction,\n\t\t\t\t\t\tdecision,\n\t\t\t\t\t\tsummary: result.summary,\n\t\t\t\t\t\tsummarySha256: createHash(\"sha256\").update(result.summary, \"utf8\").digest(\"hex\"),\n\t\t\t\t\t});\n\t\t\t\t\tconst entryId = this.deps.sessionManager.appendCompaction(\n\t\t\t\t\t\tresult.summary,\n\t\t\t\t\t\tresult.firstKeptEntryId,\n\t\t\t\t\t\tresult.tokensBefore,\n\t\t\t\t\t\tthis.detailsWithEnvelope(result.details, envelope),\n\t\t\t\t\t\tfromExtension,\n\t\t\t\t\t);\n\t\t\t\t\tconst entry = this.deps.sessionManager.getEntry(entryId);\n\t\t\t\t\tif (!entry || entry.type !== \"compaction\") {\n\t\t\t\t\t\tthrow new Error(\"Compaction commit did not produce a compaction entry\");\n\t\t\t\t\t}\n\t\t\t\t\treturn { entry, envelope };\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis.deps.recordCommit();\n\t\treturn committed;\n\t}\n}\n"]}
@@ -10,10 +10,10 @@
10
10
  import { createHash, randomUUID } from "node:crypto";
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { repairTranscriptIntegrity } from "omk-agent-core";
13
- import { sanitizeBinaryOutput } from "../utils/shell.js";
14
- import { controlStateDigest, controlStateProvenance, validateControlState, } from "./compaction/control-state.js";
15
- import { createCompactionEnvelope, createCompactionSourceIdentity, createCompactionTransaction, decideCompactionCommit, evaluateCompactionBarrier, redactCredentialShapedContent, validateCompactionEnvelope, } from "./compaction/transaction.js";
16
- import { redactSensitiveTextForced } from "./redaction.js";
13
+ import { controlStateDigest, validateControlState } from "./compaction/control-state.js";
14
+ import { decideCommitOverInertTail } from "./compaction/inert-tail.js";
15
+ import { buildPreservedProvenance, PROVENANCE_CUSTOM_TYPES } from "./compaction/provenance.js";
16
+ import { createCompactionEnvelope, createCompactionSourceIdentity, createCompactionTransaction, evaluateCompactionBarrier, validateCompactionEnvelope, } from "./compaction/transaction.js";
17
17
  import { inspectSessionIntegrity } from "./session-integrity.js";
18
18
  export class SessionCompactionService {
19
19
  deps;
@@ -21,17 +21,20 @@ export class SessionCompactionService {
21
21
  this.deps = deps;
22
22
  }
23
23
  captureState() {
24
- return this.deps.sessionManager.withCompactionCommitLock(() => this.captureStateLocked());
24
+ return this.deps.sessionManager.withCompactionCommitLock(() => this.captureStateLocked(this.readSessionBytes()));
25
25
  }
26
- captureStateLocked() {
26
+ readSessionBytes() {
27
27
  const { sessionManager } = this.deps;
28
28
  const sessionFile = sessionManager.getSessionFile();
29
- const bytes = sessionFile && existsSync(sessionFile)
29
+ return sessionFile && existsSync(sessionFile)
30
30
  ? new Uint8Array(readFileSync(sessionFile))
31
31
  : new TextEncoder().encode(`${[sessionManager.getHeader(), ...sessionManager.getEntries()]
32
32
  .filter((entry) => entry !== null)
33
33
  .map((entry) => JSON.stringify(entry))
34
34
  .join("\n")}\n`);
35
+ }
36
+ captureStateLocked(bytes) {
37
+ const { sessionManager } = this.deps;
35
38
  const report = inspectSessionIntegrity(bytes, { activeLeafId: sessionManager.getLeafId() });
36
39
  const branchEntries = report.activeBranch;
37
40
  let latestCompactionIndex = -1;
@@ -111,7 +114,7 @@ export class SessionCompactionService {
111
114
  repairedToolCallIds.add(toolResult.toolCallId);
112
115
  sessionManager.appendMessage(toolResult);
113
116
  }
114
- sessionManager.appendCustomEntry("compaction_transcript_repaired", {
117
+ sessionManager.appendCustomEntry(PROVENANCE_CUSTOM_TYPES.compactionTranscriptRepaired, {
115
118
  insertedToolCallIds: [...repairedToolCallIds],
116
119
  reason: "emergency_compaction",
117
120
  });
@@ -154,58 +157,13 @@ export class SessionCompactionService {
154
157
  source: capture.source,
155
158
  createdAt: new Date().toISOString(),
156
159
  model: { provider: compactionModel.provider, id: compactionModel.id },
157
- preserved: this.captureProvenance(capture),
160
+ preserved: buildPreservedProvenance(capture, this.deps.getUserMessageText, this.deps.cwd),
158
161
  });
159
162
  if (this.priorCommittedSourceDigests().includes(transaction.source.sourceSha256)) {
160
163
  throw new Error("This exact compaction source was already compacted");
161
164
  }
162
165
  return { capture, transaction };
163
166
  }
164
- captureProvenance(capture) {
165
- let latestIntent = "Continue the current session";
166
- for (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {
167
- const message = capture.report.activeMessages[index];
168
- if (message?.role !== "user")
169
- continue;
170
- const candidate = redactCredentialShapedContent(sanitizeBinaryOutput(redactSensitiveTextForced(this.deps.getUserMessageText(message)).trim())
171
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
172
- .slice(0, 16_384));
173
- if (candidate.length > 0)
174
- latestIntent = candidate;
175
- break;
176
- }
177
- const modelHistory = capture.branchEntries
178
- .flatMap((entry) => {
179
- if (entry.type === "model_change") {
180
- return [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];
181
- }
182
- if (entry.type === "message" && entry.message.role === "assistant") {
183
- return [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];
184
- }
185
- return [];
186
- })
187
- .slice(-256);
188
- const customEntryIds = (customType) => capture.branchEntries
189
- .filter((entry) => entry.type === "custom" && entry.customType === customType)
190
- .map((entry) => entry.id);
191
- const control = controlStateProvenance(capture.controlState);
192
- return {
193
- latestIntent,
194
- openTasks: control.openTasks,
195
- laneIds: customEntryIds("lane"),
196
- acceptancePredicateIds: customEntryIds("acceptance_predicate"),
197
- evidenceReceiptIds: customEntryIds("evidence_receipt"),
198
- blockerReasons: control.blockerReasons,
199
- repairEventIds: [
200
- ...customEntryIds("transcript_repaired"),
201
- ...customEntryIds("compaction_transcript_repaired"),
202
- ],
203
- branch: control.branch,
204
- worktree: this.deps.cwd,
205
- modelHistory,
206
- nextAction: redactCredentialShapedContent(latestIntent.slice(0, 4096)) || "Continue the current session",
207
- };
208
- }
209
167
  detailsWithEnvelope(details, envelope) {
210
168
  if (typeof details === "object" && details !== null && !Array.isArray(details)) {
211
169
  return { ...details, compactionEnvelope: envelope };
@@ -220,14 +178,19 @@ export class SessionCompactionService {
220
178
  throw new Error("Compaction first-kept entry is outside the captured source");
221
179
  }
222
180
  const committed = this.deps.sessionManager.withCompactionCommitLock(() => {
223
- const current = this.captureState();
181
+ const bytes = this.readSessionBytes();
182
+ const current = this.captureStateLocked(bytes);
224
183
  const barrier = this.evaluateBarrier(current, false);
225
- const decision = decideCompactionCommit({
184
+ const { transaction, decision } = decideCommitOverInertTail({
226
185
  transaction: begun.transaction,
227
186
  currentRevision: current.revision,
228
187
  currentSource: current.source,
188
+ currentBytes: bytes,
229
189
  barrier,
230
190
  priorCommittedSourceDigests: this.priorCommittedSourceDigests(),
191
+ // Extension state written during summarization must not livelock compaction,
192
+ // but an extension-provided summary may read that state: never rebase it.
193
+ rebaseAllowed: !fromExtension,
231
194
  });
232
195
  switch (decision.decision) {
233
196
  case "duplicate":
@@ -242,7 +205,7 @@ export class SessionCompactionService {
242
205
  throw new Error("Control state changed during compaction; generated summary was discarded");
243
206
  }
244
207
  const envelope = createCompactionEnvelope({
245
- transaction: begun.transaction,
208
+ transaction,
246
209
  decision,
247
210
  summary: result.summary,
248
211
  summarySha256: createHash("sha256").update(result.summary, "utf8").digest("hex"),
@@ -1 +1 @@
1
- {"version":3,"file":"session-compaction-service.js","sourceRoot":"","sources":["../../src/core/session-compaction-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session compaction service: capture / barrier / repair / transaction / commit.\n *\n * Extracted from AgentSession so the session keeps lifecycle orchestration\n * (abort controllers, extension events, auth, model resolution) while this\n * owns the integrity-critical compaction state machine. Every method preserves\n * the exact semantics of the original session implementation — the\n * compaction-transaction / session-integrity suites pin them.\n */\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { repairTranscriptIntegrity } from \"omk-agent-core\";\nimport type { Api, Message, Model, ToolResultMessage } from \"omk-ai\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport {\n\ttype CompactionControlState,\n\tcontrolStateDigest,\n\tcontrolStateProvenance,\n\tvalidateControlState,\n} from \"./compaction/control-state.ts\";\nimport type { CompactionResult } from \"./compaction/index.ts\";\nimport {\n\ttype CompactionBarrierResult,\n\ttype CompactionEnvelope,\n\ttype CompactionPreservedProvenanceInput,\n\ttype CompactionSourceIdentity,\n\ttype CompactionTransaction,\n\tcreateCompactionEnvelope,\n\tcreateCompactionSourceIdentity,\n\tcreateCompactionTransaction,\n\tdecideCompactionCommit,\n\tevaluateCompactionBarrier,\n\tredactCredentialShapedContent,\n\tvalidateCompactionEnvelope,\n} from \"./compaction/transaction.ts\";\nimport { redactSensitiveTextForced } from \"./redaction.ts\";\nimport type { SessionIntegrityReport } from \"./session-integrity.ts\";\nimport { inspectSessionIntegrity } from \"./session-integrity.ts\";\nimport type { CompactionEntry, SessionEntry, SessionManager } from \"./session-manager.ts\";\n\nexport interface CapturedCompactionState {\n\treadonly report: SessionIntegrityReport;\n\treadonly branchEntries: readonly SessionEntry[];\n\treadonly revision: CompactionTransaction[\"baseRevision\"];\n\treadonly source: CompactionSourceIdentity;\n\t/**\n\t * Digest of the host control-state snapshot captured under the same commit\n\t * lock as `source`. `null` when no control authority is attached. A change\n\t * between capture and commit discards the summary like a source mismatch.\n\t */\n\treadonly controlStateDigest: string | null;\n\treadonly controlState: CompactionControlState | null;\n}\n\nexport interface BegunCompaction {\n\treadonly capture: CapturedCompactionState;\n\treadonly transaction: CompactionTransaction;\n}\n\nexport interface CommittedCompaction {\n\treadonly entry: CompactionEntry;\n\treadonly envelope: CompactionEnvelope;\n}\n\nexport interface SessionCompactionServiceDeps {\n\treadonly sessionManager: SessionManager;\n\treadonly pendingToolCallIds: () => ReadonlySet<string>;\n\treadonly getUserMessageText: (message: Message) => string;\n\treadonly cwd: string;\n\t/**\n\t * Optional host control authority. Called inside the compaction commit lock\n\t * so the snapshot is bound to the same captured source as the summary. Must\n\t * be deterministic for unchanged state — a changed snapshot discards the\n\t * in-flight summary at commit. Return `null` only when no authority exists.\n\t */\n\treadonly controlState?: () => CompactionControlState | null;\n\treadonly invalidateContextBudget: () => void;\n\t/** Refresh agent messages from the session manager after tail repair/commit. */\n\treadonly refreshAgentMessages: () => void;\n\t/** Hysteresis bookkeeping after a successful commit. */\n\treadonly recordCommit: () => void;\n}\n\nexport class SessionCompactionService {\n\tprivate readonly deps: SessionCompactionServiceDeps;\n\n\tconstructor(deps: SessionCompactionServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tcaptureState(): CapturedCompactionState {\n\t\treturn this.deps.sessionManager.withCompactionCommitLock(() => this.captureStateLocked());\n\t}\n\n\tprivate captureStateLocked(): CapturedCompactionState {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst sessionFile = sessionManager.getSessionFile();\n\t\tconst bytes =\n\t\t\tsessionFile && existsSync(sessionFile)\n\t\t\t\t? new Uint8Array(readFileSync(sessionFile))\n\t\t\t\t: new TextEncoder().encode(\n\t\t\t\t\t\t`${[sessionManager.getHeader(), ...sessionManager.getEntries()]\n\t\t\t\t\t\t\t.filter((entry) => entry !== null)\n\t\t\t\t\t\t\t.map((entry) => JSON.stringify(entry))\n\t\t\t\t\t\t\t.join(\"\\n\")}\\n`,\n\t\t\t\t\t);\n\t\tconst report = inspectSessionIntegrity(bytes, { activeLeafId: sessionManager.getLeafId() });\n\t\tconst branchEntries = report.activeBranch;\n\t\tlet latestCompactionIndex = -1;\n\t\tfor (let index = branchEntries.length - 1; index >= 0; index -= 1) {\n\t\t\tif (branchEntries[index]?.type === \"compaction\") {\n\t\t\t\tlatestCompactionIndex = index;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tconst latestCompaction = branchEntries[latestCompactionIndex];\n\t\tconst firstKeptIndex =\n\t\t\tlatestCompaction?.type === \"compaction\"\n\t\t\t\t? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)\n\t\t\t\t: -1;\n\t\tconst sourceEntries = branchEntries.slice(\n\t\t\tlatestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex,\n\t\t);\n\t\tconst firstEntry = sourceEntries[0];\n\t\tconst lastEntry = sourceEntries[sourceEntries.length - 1];\n\t\tif (!firstEntry || !lastEntry || report.activeLeafId === null) {\n\t\t\tconst barrier = evaluateCompactionBarrier(report, [...this.deps.pendingToolCallIds()]);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\tthrow new Error(\"Nothing to compact: the active session branch is empty\");\n\t\t}\n\t\tconst revision = sessionManager.getDurableHeadToken();\n\t\tconst source = createCompactionSourceIdentity({\n\t\t\tsessionId: revision.sessionId,\n\t\t\tentryIds: sourceEntries.map((entry) => entry.id),\n\t\t\tfirstEntryId: firstEntry.id,\n\t\t\tlastEntryId: lastEntry.id,\n\t\t\tsourceSha256: createHash(\"sha256\")\n\t\t\t\t.update(sourceEntries.map((entry) => JSON.stringify(entry)).join(\"\\n\"), \"utf8\")\n\t\t\t\t.digest(\"hex\"),\n\t\t\tactiveLeafId: report.activeLeafId,\n\t\t\tmessageCount: report.activeMessages.length,\n\t\t});\n\t\tconst controlState = validateControlState(this.deps.controlState?.() ?? null);\n\t\treturn {\n\t\t\treport,\n\t\t\tbranchEntries,\n\t\t\trevision,\n\t\t\tsource,\n\t\t\tcontrolStateDigest: controlStateDigest(controlState),\n\t\t\tcontrolState,\n\t\t};\n\t}\n\n\tbarrierError(barrier: CompactionBarrierResult): Error {\n\t\tif (barrier.status === \"defer\") {\n\t\t\treturn new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);\n\t\t}\n\t\treturn new Error(\n\t\t\t`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`,\n\t\t);\n\t}\n\n\tevaluateBarrier(\n\t\tcapture: CapturedCompactionState,\n\t\tincludeMissingTailAsPending: boolean,\n\t\texcludedPendingIds: ReadonlySet<string> = new Set(),\n\t): CompactionBarrierResult {\n\t\tconst pending = new Set([...this.deps.pendingToolCallIds()].filter((id) => !excludedPendingIds.has(id)));\n\t\tif (includeMissingTailAsPending) {\n\t\t\tfor (const issue of capture.report.transcript?.issues ?? []) {\n\t\t\t\tif (issue.kind === \"missing_result\") pending.add(issue.toolCallId);\n\t\t\t}\n\t\t}\n\t\treturn evaluateCompactionBarrier(capture.report, [...pending]);\n\t}\n\n\trepairEmergencyTail(capture: CapturedCompactionState): {\n\t\treadonly capture: CapturedCompactionState;\n\t\treadonly repairedToolCallIds: ReadonlySet<string>;\n\t} {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst barrier = this.evaluateBarrier(capture, true);\n\t\tif (barrier.status !== \"defer\" || barrier.reason !== \"missing_active_tail_results\") {\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\treturn { capture, repairedToolCallIds: new Set() };\n\t\t}\n\t\tconst repairedMessages = repairTranscriptIntegrity(\n\t\t\t[...capture.report.activeMessages],\n\t\t\t\"Tool result missing; synthesized to close an emergency compaction barrier\",\n\t\t);\n\t\tconst inserted = repairedMessages.slice(capture.report.activeMessages.length);\n\t\tconst repairedToolCallIds = new Set<string>();\n\t\tfor (const message of inserted) {\n\t\t\tif (message.role !== \"toolResult\") {\n\t\t\t\tthrow new Error(\"Emergency compaction repair produced a non-tool result\");\n\t\t\t}\n\t\t\tconst toolResult: ToolResultMessage = message;\n\t\t\trepairedToolCallIds.add(toolResult.toolCallId);\n\t\t\tsessionManager.appendMessage(toolResult);\n\t\t}\n\t\tsessionManager.appendCustomEntry(\"compaction_transcript_repaired\", {\n\t\t\tinsertedToolCallIds: [...repairedToolCallIds],\n\t\t\treason: \"emergency_compaction\",\n\t\t});\n\t\tthis.deps.invalidateContextBudget();\n\t\tconst closedCapture = this.captureState();\n\t\tconst closedBarrier = this.evaluateBarrier(closedCapture, false, repairedToolCallIds);\n\t\tif (closedBarrier.status !== \"ready\") throw this.barrierError(closedBarrier);\n\t\tthis.deps.refreshAgentMessages();\n\t\treturn { capture: closedCapture, repairedToolCallIds };\n\t}\n\n\tpriorCommittedSourceDigests(): string[] {\n\t\tconst digests: string[] = [];\n\t\tfor (const entry of this.deps.sessionManager.getEntries()) {\n\t\t\tif (entry.type !== \"compaction\" || typeof entry.details !== \"object\" || entry.details === null) continue;\n\t\t\tif (Object.getOwnPropertyDescriptor(entry.details, \"compactionEnvelope\") === undefined) continue;\n\t\t\tconst envelope = validateCompactionEnvelope(Reflect.get(entry.details, \"compactionEnvelope\"));\n\t\t\tif (envelope.summary !== entry.summary) {\n\t\t\t\tthrow new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);\n\t\t\t}\n\t\t\tdigests.push(envelope.source.sourceSha256);\n\t\t}\n\t\treturn digests;\n\t}\n\n\tbeginTransaction(compactionModel: Model<Api>, emergency: boolean): BegunCompaction {\n\t\tlet capture = this.captureState();\n\t\tif (emergency) {\n\t\t\tcapture = this.repairEmergencyTail(capture).capture;\n\t\t} else {\n\t\t\tconst barrier = this.evaluateBarrier(capture, false);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t}\n\t\tconst transaction = createCompactionTransaction({\n\t\t\ttransactionId: randomUUID(),\n\t\t\tbaseRevision: capture.revision,\n\t\t\tsource: capture.source,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t\tmodel: { provider: compactionModel.provider, id: compactionModel.id },\n\t\t\tpreserved: this.captureProvenance(capture),\n\t\t});\n\t\tif (this.priorCommittedSourceDigests().includes(transaction.source.sourceSha256)) {\n\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t}\n\t\treturn { capture, transaction };\n\t}\n\n\tcaptureProvenance(capture: CapturedCompactionState): CompactionPreservedProvenanceInput {\n\t\tlet latestIntent = \"Continue the current session\";\n\t\tfor (let index = capture.report.activeMessages.length - 1; index >= 0; index -= 1) {\n\t\t\tconst message = capture.report.activeMessages[index];\n\t\t\tif (message?.role !== \"user\") continue;\n\t\t\tconst candidate = redactCredentialShapedContent(\n\t\t\t\tsanitizeBinaryOutput(redactSensitiveTextForced(this.deps.getUserMessageText(message)).trim())\n\t\t\t\t\t.replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n\t\t\t\t\t.slice(0, 16_384),\n\t\t\t);\n\t\t\tif (candidate.length > 0) latestIntent = candidate;\n\t\t\tbreak;\n\t\t}\n\t\tconst modelHistory = capture.branchEntries\n\t\t\t.flatMap((entry) => {\n\t\t\t\tif (entry.type === \"model_change\") {\n\t\t\t\t\treturn [{ entryId: entry.id, provider: entry.provider, modelId: entry.modelId }];\n\t\t\t\t}\n\t\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\t\treturn [{ entryId: entry.id, provider: entry.message.provider, modelId: entry.message.model }];\n\t\t\t\t}\n\t\t\t\treturn [];\n\t\t\t})\n\t\t\t.slice(-256);\n\t\tconst customEntryIds = (customType: string): string[] =>\n\t\t\tcapture.branchEntries\n\t\t\t\t.filter((entry) => entry.type === \"custom\" && entry.customType === customType)\n\t\t\t\t.map((entry) => entry.id);\n\t\tconst control = controlStateProvenance(capture.controlState);\n\t\treturn {\n\t\t\tlatestIntent,\n\t\t\topenTasks: control.openTasks,\n\t\t\tlaneIds: customEntryIds(\"lane\"),\n\t\t\tacceptancePredicateIds: customEntryIds(\"acceptance_predicate\"),\n\t\t\tevidenceReceiptIds: customEntryIds(\"evidence_receipt\"),\n\t\t\tblockerReasons: control.blockerReasons,\n\t\t\trepairEventIds: [\n\t\t\t\t...customEntryIds(\"transcript_repaired\"),\n\t\t\t\t...customEntryIds(\"compaction_transcript_repaired\"),\n\t\t\t],\n\t\t\tbranch: control.branch,\n\t\t\tworktree: this.deps.cwd,\n\t\t\tmodelHistory,\n\t\t\tnextAction: redactCredentialShapedContent(latestIntent.slice(0, 4096)) || \"Continue the current session\",\n\t\t};\n\t}\n\n\tdetailsWithEnvelope(details: unknown, envelope: CompactionEnvelope): unknown {\n\t\tif (typeof details === \"object\" && details !== null && !Array.isArray(details)) {\n\t\t\treturn { ...details, compactionEnvelope: envelope };\n\t\t}\n\t\treturn {\n\t\t\tcompactionEnvelope: envelope,\n\t\t\t...(details === undefined ? {} : { resultDetails: details }),\n\t\t};\n\t}\n\n\tcommit(begun: BegunCompaction, result: CompactionResult, fromExtension: boolean): CommittedCompaction {\n\t\tif (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {\n\t\t\tthrow new Error(\"Compaction first-kept entry is outside the captured source\");\n\t\t}\n\t\tconst committed = this.deps.sessionManager.withCompactionCommitLock(() => {\n\t\t\tconst current = this.captureState();\n\t\t\tconst barrier = this.evaluateBarrier(current, false);\n\t\t\tconst decision = decideCompactionCommit({\n\t\t\t\ttransaction: begun.transaction,\n\t\t\t\tcurrentRevision: current.revision,\n\t\t\t\tcurrentSource: current.source,\n\t\t\t\tbarrier,\n\t\t\t\tpriorCommittedSourceDigests: this.priorCommittedSourceDigests(),\n\t\t\t});\n\t\t\tswitch (decision.decision) {\n\t\t\t\tcase \"duplicate\":\n\t\t\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t\t\tcase \"stale\":\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Session changed during compaction (${decision.reason}); generated summary was discarded`,\n\t\t\t\t\t);\n\t\t\t\tcase \"defer\":\n\t\t\t\tcase \"fail_closed\":\n\t\t\t\t\tthrow this.barrierError(barrier);\n\t\t\t\tcase \"commit\": {\n\t\t\t\t\tif (current.controlStateDigest !== begun.capture.controlStateDigest) {\n\t\t\t\t\t\tthrow new Error(\"Control state changed during compaction; generated summary was discarded\");\n\t\t\t\t\t}\n\t\t\t\t\tconst envelope = createCompactionEnvelope({\n\t\t\t\t\t\ttransaction: begun.transaction,\n\t\t\t\t\t\tdecision,\n\t\t\t\t\t\tsummary: result.summary,\n\t\t\t\t\t\tsummarySha256: createHash(\"sha256\").update(result.summary, \"utf8\").digest(\"hex\"),\n\t\t\t\t\t});\n\t\t\t\t\tconst entryId = this.deps.sessionManager.appendCompaction(\n\t\t\t\t\t\tresult.summary,\n\t\t\t\t\t\tresult.firstKeptEntryId,\n\t\t\t\t\t\tresult.tokensBefore,\n\t\t\t\t\t\tthis.detailsWithEnvelope(result.details, envelope),\n\t\t\t\t\t\tfromExtension,\n\t\t\t\t\t);\n\t\t\t\t\tconst entry = this.deps.sessionManager.getEntry(entryId);\n\t\t\t\t\tif (!entry || entry.type !== \"compaction\") {\n\t\t\t\t\t\tthrow new Error(\"Compaction commit did not produce a compaction entry\");\n\t\t\t\t\t}\n\t\t\t\t\treturn { entry, envelope };\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis.deps.recordCommit();\n\t\treturn committed;\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-compaction-service.js","sourceRoot":"","sources":["../../src/core/session-compaction-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session compaction service: capture / barrier / repair / transaction / commit.\n *\n * Extracted from AgentSession so the session keeps lifecycle orchestration\n * (abort controllers, extension events, auth, model resolution) while this\n * owns the integrity-critical compaction state machine. Every method preserves\n * the exact semantics of the original session implementation — the\n * compaction-transaction / session-integrity suites pin them.\n */\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { repairTranscriptIntegrity } from \"omk-agent-core\";\nimport type { Api, Message, Model, ToolResultMessage } from \"omk-ai\";\nimport { type CompactionControlState, controlStateDigest, validateControlState } from \"./compaction/control-state.ts\";\nimport type { CompactionResult } from \"./compaction/index.ts\";\nimport { decideCommitOverInertTail } from \"./compaction/inert-tail.ts\";\nimport { buildPreservedProvenance, PROVENANCE_CUSTOM_TYPES } from \"./compaction/provenance.ts\";\nimport {\n\ttype CompactionBarrierResult,\n\ttype CompactionEnvelope,\n\ttype CompactionSourceIdentity,\n\ttype CompactionTransaction,\n\tcreateCompactionEnvelope,\n\tcreateCompactionSourceIdentity,\n\tcreateCompactionTransaction,\n\tevaluateCompactionBarrier,\n\tvalidateCompactionEnvelope,\n} from \"./compaction/transaction.ts\";\nimport type { SessionIntegrityReport } from \"./session-integrity.ts\";\nimport { inspectSessionIntegrity } from \"./session-integrity.ts\";\nimport type { CompactionEntry, SessionEntry, SessionManager } from \"./session-manager.ts\";\n\nexport interface CapturedCompactionState {\n\treadonly report: SessionIntegrityReport;\n\treadonly branchEntries: readonly SessionEntry[];\n\treadonly revision: CompactionTransaction[\"baseRevision\"];\n\treadonly source: CompactionSourceIdentity;\n\t/**\n\t * Digest of the host control-state snapshot captured under the same commit\n\t * lock as `source`. `null` when no control authority is attached. A change\n\t * between capture and commit discards the summary like a source mismatch.\n\t */\n\treadonly controlStateDigest: string | null;\n\treadonly controlState: CompactionControlState | null;\n}\n\nexport interface BegunCompaction {\n\treadonly capture: CapturedCompactionState;\n\treadonly transaction: CompactionTransaction;\n}\n\nexport interface CommittedCompaction {\n\treadonly entry: CompactionEntry;\n\treadonly envelope: CompactionEnvelope;\n}\n\nexport interface SessionCompactionServiceDeps {\n\treadonly sessionManager: SessionManager;\n\treadonly pendingToolCallIds: () => ReadonlySet<string>;\n\treadonly getUserMessageText: (message: Message) => string;\n\treadonly cwd: string;\n\t/**\n\t * Optional host control authority. Called inside the compaction commit lock\n\t * so the snapshot is bound to the same captured source as the summary. Must\n\t * be deterministic for unchanged state — a changed snapshot discards the\n\t * in-flight summary at commit. Return `null` only when no authority exists.\n\t */\n\treadonly controlState?: () => CompactionControlState | null;\n\treadonly invalidateContextBudget: () => void;\n\t/** Refresh agent messages from the session manager after tail repair/commit. */\n\treadonly refreshAgentMessages: () => void;\n\t/** Hysteresis bookkeeping after a successful commit. */\n\treadonly recordCommit: () => void;\n}\n\nexport class SessionCompactionService {\n\tprivate readonly deps: SessionCompactionServiceDeps;\n\n\tconstructor(deps: SessionCompactionServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tcaptureState(): CapturedCompactionState {\n\t\treturn this.deps.sessionManager.withCompactionCommitLock(() => this.captureStateLocked(this.readSessionBytes()));\n\t}\n\n\tprivate readSessionBytes(): Uint8Array {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst sessionFile = sessionManager.getSessionFile();\n\t\treturn sessionFile && existsSync(sessionFile)\n\t\t\t? new Uint8Array(readFileSync(sessionFile))\n\t\t\t: new TextEncoder().encode(\n\t\t\t\t\t`${[sessionManager.getHeader(), ...sessionManager.getEntries()]\n\t\t\t\t\t\t.filter((entry) => entry !== null)\n\t\t\t\t\t\t.map((entry) => JSON.stringify(entry))\n\t\t\t\t\t\t.join(\"\\n\")}\\n`,\n\t\t\t\t);\n\t}\n\n\tprivate captureStateLocked(bytes: Uint8Array): CapturedCompactionState {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst report = inspectSessionIntegrity(bytes, { activeLeafId: sessionManager.getLeafId() });\n\t\tconst branchEntries = report.activeBranch;\n\t\tlet latestCompactionIndex = -1;\n\t\tfor (let index = branchEntries.length - 1; index >= 0; index -= 1) {\n\t\t\tif (branchEntries[index]?.type === \"compaction\") {\n\t\t\t\tlatestCompactionIndex = index;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tconst latestCompaction = branchEntries[latestCompactionIndex];\n\t\tconst firstKeptIndex =\n\t\t\tlatestCompaction?.type === \"compaction\"\n\t\t\t\t? branchEntries.findIndex((entry) => entry.id === latestCompaction.firstKeptEntryId)\n\t\t\t\t: -1;\n\t\tconst sourceEntries = branchEntries.slice(\n\t\t\tlatestCompactionIndex < 0 ? 0 : firstKeptIndex < 0 ? latestCompactionIndex : firstKeptIndex,\n\t\t);\n\t\tconst firstEntry = sourceEntries[0];\n\t\tconst lastEntry = sourceEntries[sourceEntries.length - 1];\n\t\tif (!firstEntry || !lastEntry || report.activeLeafId === null) {\n\t\t\tconst barrier = evaluateCompactionBarrier(report, [...this.deps.pendingToolCallIds()]);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\tthrow new Error(\"Nothing to compact: the active session branch is empty\");\n\t\t}\n\t\tconst revision = sessionManager.getDurableHeadToken();\n\t\tconst source = createCompactionSourceIdentity({\n\t\t\tsessionId: revision.sessionId,\n\t\t\tentryIds: sourceEntries.map((entry) => entry.id),\n\t\t\tfirstEntryId: firstEntry.id,\n\t\t\tlastEntryId: lastEntry.id,\n\t\t\tsourceSha256: createHash(\"sha256\")\n\t\t\t\t.update(sourceEntries.map((entry) => JSON.stringify(entry)).join(\"\\n\"), \"utf8\")\n\t\t\t\t.digest(\"hex\"),\n\t\t\tactiveLeafId: report.activeLeafId,\n\t\t\tmessageCount: report.activeMessages.length,\n\t\t});\n\t\tconst controlState = validateControlState(this.deps.controlState?.() ?? null);\n\t\treturn {\n\t\t\treport,\n\t\t\tbranchEntries,\n\t\t\trevision,\n\t\t\tsource,\n\t\t\tcontrolStateDigest: controlStateDigest(controlState),\n\t\t\tcontrolState,\n\t\t};\n\t}\n\n\tbarrierError(barrier: CompactionBarrierResult): Error {\n\t\tif (barrier.status === \"defer\") {\n\t\t\treturn new Error(`Compaction deferred until the transcript closes (${barrier.reason})`);\n\t\t}\n\t\treturn new Error(\n\t\t\t`Compaction failed closed on transcript integrity (${barrier.reason}). Run the session doctor before retrying.`,\n\t\t);\n\t}\n\n\tevaluateBarrier(\n\t\tcapture: CapturedCompactionState,\n\t\tincludeMissingTailAsPending: boolean,\n\t\texcludedPendingIds: ReadonlySet<string> = new Set(),\n\t): CompactionBarrierResult {\n\t\tconst pending = new Set([...this.deps.pendingToolCallIds()].filter((id) => !excludedPendingIds.has(id)));\n\t\tif (includeMissingTailAsPending) {\n\t\t\tfor (const issue of capture.report.transcript?.issues ?? []) {\n\t\t\t\tif (issue.kind === \"missing_result\") pending.add(issue.toolCallId);\n\t\t\t}\n\t\t}\n\t\treturn evaluateCompactionBarrier(capture.report, [...pending]);\n\t}\n\n\trepairEmergencyTail(capture: CapturedCompactionState): {\n\t\treadonly capture: CapturedCompactionState;\n\t\treadonly repairedToolCallIds: ReadonlySet<string>;\n\t} {\n\t\tconst { sessionManager } = this.deps;\n\t\tconst barrier = this.evaluateBarrier(capture, true);\n\t\tif (barrier.status !== \"defer\" || barrier.reason !== \"missing_active_tail_results\") {\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t\treturn { capture, repairedToolCallIds: new Set() };\n\t\t}\n\t\tconst repairedMessages = repairTranscriptIntegrity(\n\t\t\t[...capture.report.activeMessages],\n\t\t\t\"Tool result missing; synthesized to close an emergency compaction barrier\",\n\t\t);\n\t\tconst inserted = repairedMessages.slice(capture.report.activeMessages.length);\n\t\tconst repairedToolCallIds = new Set<string>();\n\t\tfor (const message of inserted) {\n\t\t\tif (message.role !== \"toolResult\") {\n\t\t\t\tthrow new Error(\"Emergency compaction repair produced a non-tool result\");\n\t\t\t}\n\t\t\tconst toolResult: ToolResultMessage = message;\n\t\t\trepairedToolCallIds.add(toolResult.toolCallId);\n\t\t\tsessionManager.appendMessage(toolResult);\n\t\t}\n\t\tsessionManager.appendCustomEntry(PROVENANCE_CUSTOM_TYPES.compactionTranscriptRepaired, {\n\t\t\tinsertedToolCallIds: [...repairedToolCallIds],\n\t\t\treason: \"emergency_compaction\",\n\t\t});\n\t\tthis.deps.invalidateContextBudget();\n\t\tconst closedCapture = this.captureState();\n\t\tconst closedBarrier = this.evaluateBarrier(closedCapture, false, repairedToolCallIds);\n\t\tif (closedBarrier.status !== \"ready\") throw this.barrierError(closedBarrier);\n\t\tthis.deps.refreshAgentMessages();\n\t\treturn { capture: closedCapture, repairedToolCallIds };\n\t}\n\n\tpriorCommittedSourceDigests(): string[] {\n\t\tconst digests: string[] = [];\n\t\tfor (const entry of this.deps.sessionManager.getEntries()) {\n\t\t\tif (entry.type !== \"compaction\" || typeof entry.details !== \"object\" || entry.details === null) continue;\n\t\t\tif (Object.getOwnPropertyDescriptor(entry.details, \"compactionEnvelope\") === undefined) continue;\n\t\t\tconst envelope = validateCompactionEnvelope(Reflect.get(entry.details, \"compactionEnvelope\"));\n\t\t\tif (envelope.summary !== entry.summary) {\n\t\t\t\tthrow new Error(`Compaction entry ${entry.id} has invalid provenance. Run the session doctor.`);\n\t\t\t}\n\t\t\tdigests.push(envelope.source.sourceSha256);\n\t\t}\n\t\treturn digests;\n\t}\n\n\tbeginTransaction(compactionModel: Model<Api>, emergency: boolean): BegunCompaction {\n\t\tlet capture = this.captureState();\n\t\tif (emergency) {\n\t\t\tcapture = this.repairEmergencyTail(capture).capture;\n\t\t} else {\n\t\t\tconst barrier = this.evaluateBarrier(capture, false);\n\t\t\tif (barrier.status !== \"ready\") throw this.barrierError(barrier);\n\t\t}\n\t\tconst transaction = createCompactionTransaction({\n\t\t\ttransactionId: randomUUID(),\n\t\t\tbaseRevision: capture.revision,\n\t\t\tsource: capture.source,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t\tmodel: { provider: compactionModel.provider, id: compactionModel.id },\n\t\t\tpreserved: buildPreservedProvenance(capture, this.deps.getUserMessageText, this.deps.cwd),\n\t\t});\n\t\tif (this.priorCommittedSourceDigests().includes(transaction.source.sourceSha256)) {\n\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t}\n\t\treturn { capture, transaction };\n\t}\n\n\tdetailsWithEnvelope(details: unknown, envelope: CompactionEnvelope): unknown {\n\t\tif (typeof details === \"object\" && details !== null && !Array.isArray(details)) {\n\t\t\treturn { ...details, compactionEnvelope: envelope };\n\t\t}\n\t\treturn {\n\t\t\tcompactionEnvelope: envelope,\n\t\t\t...(details === undefined ? {} : { resultDetails: details }),\n\t\t};\n\t}\n\n\tcommit(begun: BegunCompaction, result: CompactionResult, fromExtension: boolean): CommittedCompaction {\n\t\tif (!begun.transaction.source.entryIds.includes(result.firstKeptEntryId)) {\n\t\t\tthrow new Error(\"Compaction first-kept entry is outside the captured source\");\n\t\t}\n\t\tconst committed = this.deps.sessionManager.withCompactionCommitLock(() => {\n\t\t\tconst bytes = this.readSessionBytes();\n\t\t\tconst current = this.captureStateLocked(bytes);\n\t\t\tconst barrier = this.evaluateBarrier(current, false);\n\t\t\tconst { transaction, decision } = decideCommitOverInertTail({\n\t\t\t\ttransaction: begun.transaction,\n\t\t\t\tcurrentRevision: current.revision,\n\t\t\t\tcurrentSource: current.source,\n\t\t\t\tcurrentBytes: bytes,\n\t\t\t\tbarrier,\n\t\t\t\tpriorCommittedSourceDigests: this.priorCommittedSourceDigests(),\n\t\t\t\t// Extension state written during summarization must not livelock compaction,\n\t\t\t\t// but an extension-provided summary may read that state: never rebase it.\n\t\t\t\trebaseAllowed: !fromExtension,\n\t\t\t});\n\t\t\tswitch (decision.decision) {\n\t\t\t\tcase \"duplicate\":\n\t\t\t\t\tthrow new Error(\"This exact compaction source was already compacted\");\n\t\t\t\tcase \"stale\":\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Session changed during compaction (${decision.reason}); generated summary was discarded`,\n\t\t\t\t\t);\n\t\t\t\tcase \"defer\":\n\t\t\t\tcase \"fail_closed\":\n\t\t\t\t\tthrow this.barrierError(barrier);\n\t\t\t\tcase \"commit\": {\n\t\t\t\t\tif (current.controlStateDigest !== begun.capture.controlStateDigest) {\n\t\t\t\t\t\tthrow new Error(\"Control state changed during compaction; generated summary was discarded\");\n\t\t\t\t\t}\n\t\t\t\t\tconst envelope = createCompactionEnvelope({\n\t\t\t\t\t\ttransaction,\n\t\t\t\t\t\tdecision,\n\t\t\t\t\t\tsummary: result.summary,\n\t\t\t\t\t\tsummarySha256: createHash(\"sha256\").update(result.summary, \"utf8\").digest(\"hex\"),\n\t\t\t\t\t});\n\t\t\t\t\tconst entryId = this.deps.sessionManager.appendCompaction(\n\t\t\t\t\t\tresult.summary,\n\t\t\t\t\t\tresult.firstKeptEntryId,\n\t\t\t\t\t\tresult.tokensBefore,\n\t\t\t\t\t\tthis.detailsWithEnvelope(result.details, envelope),\n\t\t\t\t\t\tfromExtension,\n\t\t\t\t\t);\n\t\t\t\t\tconst entry = this.deps.sessionManager.getEntry(entryId);\n\t\t\t\t\tif (!entry || entry.type !== \"compaction\") {\n\t\t\t\t\t\tthrow new Error(\"Compaction commit did not produce a compaction entry\");\n\t\t\t\t\t}\n\t\t\t\t\treturn { entry, envelope };\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis.deps.recordCommit();\n\t\treturn committed;\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"owned-execution.d.ts","sourceRoot":"","sources":["../../../src/core/verified-run/owned-execution.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIvD,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AACD,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAkCD;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE;IACP,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC9B,GACC,OAAO,CAAC,cAAc,CAAC,CA2FzB","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { realpathSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport type { ResourceClaimInput } from \"../../coordination/resource.ts\";\nimport type { GrantToken } from \"../../coordination/types.ts\";\nimport type { RunAuthority } from \"./authority-runtime.ts\";\nimport { executeSandbox, type SandboxOutcome } from \"./broker.ts\";\nimport type { VerifiedRunJournal } from \"./journal.ts\";\nimport type { NamespaceIdentity } from \"./namespace-identity.ts\";\nimport { digestObject, VerifiedRunError } from \"./storage.ts\";\n\nexport interface OwnedRunCommand {\n\treadonly role: \"writer\" | \"verifier\";\n\treadonly argv: readonly string[];\n\treadonly workspace: string;\n\treadonly deadline: number;\n\treadonly claimId: string | null;\n\treadonly taskId?: string;\n}\nexport interface OwnedRunResult {\n\treadonly executionId: string;\n\treadonly result: SandboxOutcome;\n\treadonly timeoutMs: number;\n}\n\n/**\n * The filesystem claim a dispatched process is authorized for: exactly the\n * workspace bind it receives inside the namespace — nothing wider. Keys are\n * the resolved absolute path minus the leading slash so prefix containment\n * matches real tree containment; `realpathSync` keeps two spellings of the\n * same directory one claim.\n */\nfunction workspaceClaim(\n\tworkspace: string,\n\taccess: \"read\" | \"write\",\n\tgeneration: number,\n): readonly ResourceClaimInput[] {\n\tlet root: string;\n\ttry {\n\t\troot = realpathSync(workspace);\n\t} catch {\n\t\tthrow new VerifiedRunError(\"authority_claim\");\n\t}\n\treturn Object.freeze([\n\t\t{\n\t\t\tnamespace: \"filesystem\",\n\t\t\tinstanceId: \"verified-run\",\n\t\t\tcanonicalKey: root.replace(/^\\/+/, \"\"),\n\t\t\taccess,\n\t\t\tgeneration: String(generation),\n\t\t},\n\t]);\n}\n\n/** Rejection codes that can only be produced before the supervisor spawned. */\nconst NEVER_SPAWNED = new Set([\"cancelled\", \"deadline\", \"unsupported_boundary\"]);\n\n/**\n * Mandatory durable dispatch intent, shared by command and AgentSession\n * writers. There is no ungated execution lane: a journal without an anchored\n * budget cannot commit a process identity, so the supervisor boundary would\n * be unprovable and must refuse dispatch. Every dispatch now also crosses the\n * durable authority boundary — grant → intent → start → termination witness\n * — so a restart can reconcile effects the journal alone cannot settle.\n */\nexport async function executeRunCommand(\n\tjournal: VerifiedRunJournal,\n\trequest: OwnedRunCommand,\n\tpolicy: {\n\t\treadonly cleanupMs: number;\n\t\treadonly maxOutputBytes: number;\n\t\treadonly authority: RunAuthority;\n\t\treadonly signal?: AbortSignal;\n\t},\n): Promise<OwnedRunResult> {\n\tif (Math.floor(request.deadline - performance.now()) <= 0) throw new VerifiedRunError(\"deadline\");\n\tif (policy.signal?.aborted) throw new VerifiedRunError(\"cancelled\");\n\t// The supervisor boundary only exists once a budget (and therefore the\n\t// gated driver) is anchored. Dispatching without it would run code whose\n\t// termination cannot be witnessed — refuse rather than degrade.\n\tif (!journal.state.budget) throw new VerifiedRunError(\"unsupported_boundary\");\n\tconst authority = policy.authority;\n\tconst generation = journal.state.generation;\n\tconst executionId = randomUUID();\n\tconst claims = workspaceClaim(request.workspace, request.role === \"writer\" ? \"write\" : \"read\", generation);\n\tconst timeoutMs = Math.floor(request.deadline - performance.now());\n\tconst admission = authority.store.acquire({\n\t\tsessionId: authority.sessionId,\n\t\tincarnation: authority.incarnation,\n\t\tcommandId: executionId,\n\t\tintentDigest: digestObject({ argv: request.argv, claims, role: request.role }),\n\t\tclaims,\n\t\tnow: Date.now(),\n\t\tttl: timeoutMs + policy.cleanupMs + 30000,\n\t});\n\tif (admission.status !== \"granted\") {\n\t\t// A fresh executionId can only collide with a conflicting or wedged\n\t\t// claim — quarantined effects hold theirs until witnessed (fail closed).\n\t\tthrow new VerifiedRunError(\"authority_blocked\");\n\t}\n\tconst token: GrantToken = admission.token;\n\tlet supervisorInvoked = false;\n\tlet readyObserved = false;\n\tlet result: SandboxOutcome;\n\ttry {\n\t\tjournal.append({\n\t\t\tkind: \"dispatch\",\n\t\t\texecutionId,\n\t\t\trole: request.role,\n\t\t\tclaimId: request.claimId,\n\t\t\t...(request.taskId === undefined ? {} : { taskId: request.taskId }),\n\t\t});\n\t\tif (timeoutMs <= 0 || policy.signal?.aborted) {\n\t\t\tconst failure = policy.signal?.aborted ? \"cancelled\" : \"deadline\";\n\t\t\t// Provably pre-spawn: the intent never reached the adapter, so the grant\n\t\t\t// settles immediately instead of wedging as an unwitnessed live effect.\n\t\t\tauthority.store.cancel(token);\n\t\t\tauthority.store.confirmTerminated(token);\n\t\t\tjournal.append({ kind: \"exited\", executionId, failure });\n\t\t\tthrow new VerifiedRunError(failure);\n\t\t}\n\t\t// The intent commits before the adapter is invoked: from this record on,\n\t\t// the effect may be live and a crash must quarantine it, never assume\n\t\t// it did not run.\n\t\tif (!authority.store.dispatchIntent(token, executionId)) throw new VerifiedRunError(\"authority\");\n\t\tsupervisorInvoked = true;\n\t\tresult = await executeSandbox({\n\t\t\tworkspace: request.workspace,\n\t\t\targv: request.argv,\n\t\t\twritable: request.role === \"writer\",\n\t\t\ttimeoutMs: Math.floor(request.deadline - performance.now()),\n\t\t\tcleanupMs: policy.cleanupMs,\n\t\t\tmaxOutputBytes: policy.maxOutputBytes,\n\t\t\t...(policy.signal ? { signal: policy.signal } : {}),\n\t\t\tonReady: (identity: NamespaceIdentity) => {\n\t\t\t\treadyObserved = true;\n\t\t\t\tif (journal.state.generation !== generation) throw new VerifiedRunError(\"stale_generation\");\n\t\t\t\tjournal.append({ kind: \"process_ready\", executionId, identity });\n\t\t\t\tif (!authority.store.effectStarted(token, claims, identity)) throw new VerifiedRunError(\"authority\");\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\t// Only pre-spawn rejections prove nothing ran; everything else may have\n\t\t// a live or dying effect, which stays owned until a witness or a later\n\t\t// reconcile settles it.\n\t\ttry {\n\t\t\tauthority.store.cancel(token);\n\t\t\tif (\n\t\t\t\t!supervisorInvoked ||\n\t\t\t\t(!readyObserved && error instanceof VerifiedRunError && NEVER_SPAWNED.has(error.code))\n\t\t\t)\n\t\t\t\tauthority.store.confirmTerminated(token);\n\t\t} catch (cleanupError) {\n\t\t\tthrow new AggregateError([error, cleanupError], \"Execution ownership cleanup failed\");\n\t\t}\n\t\tthrow error;\n\t}\n\tif (result.failure === \"cancelled\") authority.store.cancel(token);\n\t// Resolve means both witnesses fired: the direct child closed and the\n\t// recorded namespace drained empty. That is the termination-observed event\n\t// — the grant releases its claims here and nowhere else.\n\tif (!authority.store.confirmTerminated(token)) throw new VerifiedRunError(\"authority\");\n\tif (journal.state.generation !== generation) throw new VerifiedRunError(\"stale_generation\");\n\tjournal.append({ kind: \"exited\", executionId, failure: request.role === \"writer\" ? result.failure : null });\n\treturn { executionId, result, timeoutMs };\n}\n"]}
1
+ {"version":3,"file":"owned-execution.d.ts","sourceRoot":"","sources":["../../../src/core/verified-run/owned-execution.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIvD,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AACD,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAkCD;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE;IACP,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC9B,GACC,OAAO,CAAC,cAAc,CAAC,CAuGzB","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { realpathSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport type { ResourceClaimInput } from \"../../coordination/resource.ts\";\nimport type { GrantToken } from \"../../coordination/types.ts\";\nimport type { RunAuthority } from \"./authority-runtime.ts\";\nimport { executeSandbox, type SandboxOutcome } from \"./broker.ts\";\nimport type { VerifiedRunJournal } from \"./journal.ts\";\nimport type { NamespaceIdentity } from \"./namespace-identity.ts\";\nimport { digestObject, VerifiedRunError } from \"./storage.ts\";\n\nexport interface OwnedRunCommand {\n\treadonly role: \"writer\" | \"verifier\";\n\treadonly argv: readonly string[];\n\treadonly workspace: string;\n\treadonly deadline: number;\n\treadonly claimId: string | null;\n\treadonly taskId?: string;\n}\nexport interface OwnedRunResult {\n\treadonly executionId: string;\n\treadonly result: SandboxOutcome;\n\treadonly timeoutMs: number;\n}\n\n/**\n * The filesystem claim a dispatched process is authorized for: exactly the\n * workspace bind it receives inside the namespace — nothing wider. Keys are\n * the resolved absolute path minus the leading slash so prefix containment\n * matches real tree containment; `realpathSync` keeps two spellings of the\n * same directory one claim.\n */\nfunction workspaceClaim(\n\tworkspace: string,\n\taccess: \"read\" | \"write\",\n\tgeneration: number,\n): readonly ResourceClaimInput[] {\n\tlet root: string;\n\ttry {\n\t\troot = realpathSync(workspace);\n\t} catch {\n\t\tthrow new VerifiedRunError(\"authority_claim\");\n\t}\n\treturn Object.freeze([\n\t\t{\n\t\t\tnamespace: \"filesystem\",\n\t\t\tinstanceId: \"verified-run\",\n\t\t\tcanonicalKey: root.replace(/^\\/+/, \"\"),\n\t\t\taccess,\n\t\t\tgeneration: String(generation),\n\t\t},\n\t]);\n}\n\n/** Rejection codes that can only be produced before the supervisor spawned. */\nconst NEVER_SPAWNED = new Set([\"cancelled\", \"deadline\", \"unsupported_boundary\"]);\n\n/**\n * Mandatory durable dispatch intent, shared by command and AgentSession\n * writers. There is no ungated execution lane: a journal without an anchored\n * budget cannot commit a process identity, so the supervisor boundary would\n * be unprovable and must refuse dispatch. Every dispatch now also crosses the\n * durable authority boundary — grant → intent → start → termination witness\n * — so a restart can reconcile effects the journal alone cannot settle.\n */\nexport async function executeRunCommand(\n\tjournal: VerifiedRunJournal,\n\trequest: OwnedRunCommand,\n\tpolicy: {\n\t\treadonly cleanupMs: number;\n\t\treadonly maxOutputBytes: number;\n\t\treadonly authority: RunAuthority;\n\t\treadonly signal?: AbortSignal;\n\t},\n): Promise<OwnedRunResult> {\n\tif (Math.floor(request.deadline - performance.now()) <= 0) throw new VerifiedRunError(\"deadline\");\n\tif (policy.signal?.aborted) throw new VerifiedRunError(\"cancelled\");\n\t// The supervisor boundary only exists once a budget (and therefore the\n\t// gated driver) is anchored. Dispatching without it would run code whose\n\t// termination cannot be witnessed — refuse rather than degrade.\n\tif (!journal.state.budget) throw new VerifiedRunError(\"unsupported_boundary\");\n\tconst authority = policy.authority;\n\tconst generation = journal.state.generation;\n\tconst executionId = randomUUID();\n\tconst claims = workspaceClaim(request.workspace, request.role === \"writer\" ? \"write\" : \"read\", generation);\n\tconst timeoutMs = Math.floor(request.deadline - performance.now());\n\tconst admission = authority.store.acquire({\n\t\tsessionId: authority.sessionId,\n\t\tincarnation: authority.incarnation,\n\t\tcommandId: executionId,\n\t\tintentDigest: digestObject({ argv: request.argv, claims, role: request.role }),\n\t\tclaims,\n\t\tnow: Date.now(),\n\t\tttl: timeoutMs + policy.cleanupMs + 30000,\n\t});\n\tif (admission.status !== \"granted\") {\n\t\t// A fresh executionId can only collide with a conflicting or wedged\n\t\t// claim — quarantined effects hold theirs until witnessed (fail closed).\n\t\tthrow new VerifiedRunError(\"authority_blocked\");\n\t}\n\tconst token: GrantToken = admission.token;\n\tlet supervisorInvoked = false;\n\tlet readyObserved = false;\n\tlet result: SandboxOutcome;\n\ttry {\n\t\tjournal.append({\n\t\t\tkind: \"dispatch\",\n\t\t\texecutionId,\n\t\t\trole: request.role,\n\t\t\tclaimId: request.claimId,\n\t\t\t...(request.taskId === undefined ? {} : { taskId: request.taskId }),\n\t\t});\n\t\tif (timeoutMs <= 0 || policy.signal?.aborted) {\n\t\t\tconst failure = policy.signal?.aborted ? \"cancelled\" : \"deadline\";\n\t\t\t// Provably pre-spawn: the intent never reached the adapter, so the grant\n\t\t\t// settles immediately instead of wedging as an unwitnessed live effect.\n\t\t\tauthority.store.cancel(token);\n\t\t\tauthority.store.confirmTerminated(token);\n\t\t\tjournal.append({ kind: \"exited\", executionId, failure });\n\t\t\tthrow new VerifiedRunError(failure);\n\t\t}\n\t\t// The intent commits before the adapter is invoked: from this record on,\n\t\t// the effect may be live and a crash must quarantine it, never assume\n\t\t// it did not run.\n\t\tif (!authority.store.dispatchIntent(token, executionId)) throw new VerifiedRunError(\"authority\");\n\t\tsupervisorInvoked = true;\n\t\tresult = await executeSandbox({\n\t\t\tworkspace: request.workspace,\n\t\t\targv: request.argv,\n\t\t\twritable: request.role === \"writer\",\n\t\t\ttimeoutMs: Math.floor(request.deadline - performance.now()),\n\t\t\tcleanupMs: policy.cleanupMs,\n\t\t\tmaxOutputBytes: policy.maxOutputBytes,\n\t\t\t...(policy.signal ? { signal: policy.signal } : {}),\n\t\t\tonReady: (identity: NamespaceIdentity) => {\n\t\t\t\treadyObserved = true;\n\t\t\t\tif (journal.state.generation !== generation) throw new VerifiedRunError(\"stale_generation\");\n\t\t\t\tjournal.append({ kind: \"process_ready\", executionId, identity });\n\t\t\t\tif (!authority.store.effectStarted(token, claims, identity)) throw new VerifiedRunError(\"authority\");\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\t// Only pre-spawn rejections prove nothing ran; everything else may have\n\t\t// a live or dying effect, which stays owned until a witness or a later\n\t\t// reconcile settles it.\n\t\ttry {\n\t\t\tauthority.store.cancel(token);\n\t\t\tif (\n\t\t\t\t!supervisorInvoked ||\n\t\t\t\t(!readyObserved && error instanceof VerifiedRunError && NEVER_SPAWNED.has(error.code))\n\t\t\t) {\n\t\t\t\tauthority.store.confirmTerminated(token);\n\t\t\t\t// The intent provably never reached a process: journal the exit the same\n\t\t\t\t// way the pre-spawn timeout branch does, or the run quarantines forever\n\t\t\t\t// on an open executionId — the slow-runner flake where the second\n\t\t\t\t// deadline evaluation crosses zero between dispatch and spawn.\n\t\t\t\tif (journal.state.activeExecutionIds.includes(executionId)) {\n\t\t\t\t\tjournal.append({\n\t\t\t\t\t\tkind: \"exited\",\n\t\t\t\t\t\texecutionId,\n\t\t\t\t\t\tfailure: error instanceof VerifiedRunError ? error.code : \"execution_failed\",\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (cleanupError) {\n\t\t\tthrow new AggregateError([error, cleanupError], \"Execution ownership cleanup failed\");\n\t\t}\n\t\tthrow error;\n\t}\n\tif (result.failure === \"cancelled\") authority.store.cancel(token);\n\t// Resolve means both witnesses fired: the direct child closed and the\n\t// recorded namespace drained empty. That is the termination-observed event\n\t// — the grant releases its claims here and nowhere else.\n\tif (!authority.store.confirmTerminated(token)) throw new VerifiedRunError(\"authority\");\n\tif (journal.state.generation !== generation) throw new VerifiedRunError(\"stale_generation\");\n\tjournal.append({ kind: \"exited\", executionId, failure: request.role === \"writer\" ? result.failure : null });\n\treturn { executionId, result, timeoutMs };\n}\n"]}
@@ -119,8 +119,20 @@ export async function executeRunCommand(journal, request, policy) {
119
119
  try {
120
120
  authority.store.cancel(token);
121
121
  if (!supervisorInvoked ||
122
- (!readyObserved && error instanceof VerifiedRunError && NEVER_SPAWNED.has(error.code)))
122
+ (!readyObserved && error instanceof VerifiedRunError && NEVER_SPAWNED.has(error.code))) {
123
123
  authority.store.confirmTerminated(token);
124
+ // The intent provably never reached a process: journal the exit the same
125
+ // way the pre-spawn timeout branch does, or the run quarantines forever
126
+ // on an open executionId — the slow-runner flake where the second
127
+ // deadline evaluation crosses zero between dispatch and spawn.
128
+ if (journal.state.activeExecutionIds.includes(executionId)) {
129
+ journal.append({
130
+ kind: "exited",
131
+ executionId,
132
+ failure: error instanceof VerifiedRunError ? error.code : "execution_failed",
133
+ });
134
+ }
135
+ }
124
136
  }
125
137
  catch (cleanupError) {
126
138
  throw new AggregateError([error, cleanupError], "Execution ownership cleanup failed");
@@ -1 +1 @@
1
- {"version":3,"file":"owned-execution.js","sourceRoot":"","sources":["../../../src/core/verified-run/owned-execution.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { realpathSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport type { ResourceClaimInput } from \"../../coordination/resource.ts\";\nimport type { GrantToken } from \"../../coordination/types.ts\";\nimport type { RunAuthority } from \"./authority-runtime.ts\";\nimport { executeSandbox, type SandboxOutcome } from \"./broker.ts\";\nimport type { VerifiedRunJournal } from \"./journal.ts\";\nimport type { NamespaceIdentity } from \"./namespace-identity.ts\";\nimport { digestObject, VerifiedRunError } from \"./storage.ts\";\n\nexport interface OwnedRunCommand {\n\treadonly role: \"writer\" | \"verifier\";\n\treadonly argv: readonly string[];\n\treadonly workspace: string;\n\treadonly deadline: number;\n\treadonly claimId: string | null;\n\treadonly taskId?: string;\n}\nexport interface OwnedRunResult {\n\treadonly executionId: string;\n\treadonly result: SandboxOutcome;\n\treadonly timeoutMs: number;\n}\n\n/**\n * The filesystem claim a dispatched process is authorized for: exactly the\n * workspace bind it receives inside the namespace — nothing wider. Keys are\n * the resolved absolute path minus the leading slash so prefix containment\n * matches real tree containment; `realpathSync` keeps two spellings of the\n * same directory one claim.\n */\nfunction workspaceClaim(\n\tworkspace: string,\n\taccess: \"read\" | \"write\",\n\tgeneration: number,\n): readonly ResourceClaimInput[] {\n\tlet root: string;\n\ttry {\n\t\troot = realpathSync(workspace);\n\t} catch {\n\t\tthrow new VerifiedRunError(\"authority_claim\");\n\t}\n\treturn Object.freeze([\n\t\t{\n\t\t\tnamespace: \"filesystem\",\n\t\t\tinstanceId: \"verified-run\",\n\t\t\tcanonicalKey: root.replace(/^\\/+/, \"\"),\n\t\t\taccess,\n\t\t\tgeneration: String(generation),\n\t\t},\n\t]);\n}\n\n/** Rejection codes that can only be produced before the supervisor spawned. */\nconst NEVER_SPAWNED = new Set([\"cancelled\", \"deadline\", \"unsupported_boundary\"]);\n\n/**\n * Mandatory durable dispatch intent, shared by command and AgentSession\n * writers. There is no ungated execution lane: a journal without an anchored\n * budget cannot commit a process identity, so the supervisor boundary would\n * be unprovable and must refuse dispatch. Every dispatch now also crosses the\n * durable authority boundary — grant → intent → start → termination witness\n * — so a restart can reconcile effects the journal alone cannot settle.\n */\nexport async function executeRunCommand(\n\tjournal: VerifiedRunJournal,\n\trequest: OwnedRunCommand,\n\tpolicy: {\n\t\treadonly cleanupMs: number;\n\t\treadonly maxOutputBytes: number;\n\t\treadonly authority: RunAuthority;\n\t\treadonly signal?: AbortSignal;\n\t},\n): Promise<OwnedRunResult> {\n\tif (Math.floor(request.deadline - performance.now()) <= 0) throw new VerifiedRunError(\"deadline\");\n\tif (policy.signal?.aborted) throw new VerifiedRunError(\"cancelled\");\n\t// The supervisor boundary only exists once a budget (and therefore the\n\t// gated driver) is anchored. Dispatching without it would run code whose\n\t// termination cannot be witnessed — refuse rather than degrade.\n\tif (!journal.state.budget) throw new VerifiedRunError(\"unsupported_boundary\");\n\tconst authority = policy.authority;\n\tconst generation = journal.state.generation;\n\tconst executionId = randomUUID();\n\tconst claims = workspaceClaim(request.workspace, request.role === \"writer\" ? \"write\" : \"read\", generation);\n\tconst timeoutMs = Math.floor(request.deadline - performance.now());\n\tconst admission = authority.store.acquire({\n\t\tsessionId: authority.sessionId,\n\t\tincarnation: authority.incarnation,\n\t\tcommandId: executionId,\n\t\tintentDigest: digestObject({ argv: request.argv, claims, role: request.role }),\n\t\tclaims,\n\t\tnow: Date.now(),\n\t\tttl: timeoutMs + policy.cleanupMs + 30000,\n\t});\n\tif (admission.status !== \"granted\") {\n\t\t// A fresh executionId can only collide with a conflicting or wedged\n\t\t// claim — quarantined effects hold theirs until witnessed (fail closed).\n\t\tthrow new VerifiedRunError(\"authority_blocked\");\n\t}\n\tconst token: GrantToken = admission.token;\n\tlet supervisorInvoked = false;\n\tlet readyObserved = false;\n\tlet result: SandboxOutcome;\n\ttry {\n\t\tjournal.append({\n\t\t\tkind: \"dispatch\",\n\t\t\texecutionId,\n\t\t\trole: request.role,\n\t\t\tclaimId: request.claimId,\n\t\t\t...(request.taskId === undefined ? {} : { taskId: request.taskId }),\n\t\t});\n\t\tif (timeoutMs <= 0 || policy.signal?.aborted) {\n\t\t\tconst failure = policy.signal?.aborted ? \"cancelled\" : \"deadline\";\n\t\t\t// Provably pre-spawn: the intent never reached the adapter, so the grant\n\t\t\t// settles immediately instead of wedging as an unwitnessed live effect.\n\t\t\tauthority.store.cancel(token);\n\t\t\tauthority.store.confirmTerminated(token);\n\t\t\tjournal.append({ kind: \"exited\", executionId, failure });\n\t\t\tthrow new VerifiedRunError(failure);\n\t\t}\n\t\t// The intent commits before the adapter is invoked: from this record on,\n\t\t// the effect may be live and a crash must quarantine it, never assume\n\t\t// it did not run.\n\t\tif (!authority.store.dispatchIntent(token, executionId)) throw new VerifiedRunError(\"authority\");\n\t\tsupervisorInvoked = true;\n\t\tresult = await executeSandbox({\n\t\t\tworkspace: request.workspace,\n\t\t\targv: request.argv,\n\t\t\twritable: request.role === \"writer\",\n\t\t\ttimeoutMs: Math.floor(request.deadline - performance.now()),\n\t\t\tcleanupMs: policy.cleanupMs,\n\t\t\tmaxOutputBytes: policy.maxOutputBytes,\n\t\t\t...(policy.signal ? { signal: policy.signal } : {}),\n\t\t\tonReady: (identity: NamespaceIdentity) => {\n\t\t\t\treadyObserved = true;\n\t\t\t\tif (journal.state.generation !== generation) throw new VerifiedRunError(\"stale_generation\");\n\t\t\t\tjournal.append({ kind: \"process_ready\", executionId, identity });\n\t\t\t\tif (!authority.store.effectStarted(token, claims, identity)) throw new VerifiedRunError(\"authority\");\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\t// Only pre-spawn rejections prove nothing ran; everything else may have\n\t\t// a live or dying effect, which stays owned until a witness or a later\n\t\t// reconcile settles it.\n\t\ttry {\n\t\t\tauthority.store.cancel(token);\n\t\t\tif (\n\t\t\t\t!supervisorInvoked ||\n\t\t\t\t(!readyObserved && error instanceof VerifiedRunError && NEVER_SPAWNED.has(error.code))\n\t\t\t)\n\t\t\t\tauthority.store.confirmTerminated(token);\n\t\t} catch (cleanupError) {\n\t\t\tthrow new AggregateError([error, cleanupError], \"Execution ownership cleanup failed\");\n\t\t}\n\t\tthrow error;\n\t}\n\tif (result.failure === \"cancelled\") authority.store.cancel(token);\n\t// Resolve means both witnesses fired: the direct child closed and the\n\t// recorded namespace drained empty. That is the termination-observed event\n\t// — the grant releases its claims here and nowhere else.\n\tif (!authority.store.confirmTerminated(token)) throw new VerifiedRunError(\"authority\");\n\tif (journal.state.generation !== generation) throw new VerifiedRunError(\"stale_generation\");\n\tjournal.append({ kind: \"exited\", executionId, failure: request.role === \"writer\" ? result.failure : null });\n\treturn { executionId, result, timeoutMs };\n}\n"]}
1
+ {"version":3,"file":"owned-execution.js","sourceRoot":"","sources":["../../../src/core/verified-run/owned-execution.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { realpathSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport type { ResourceClaimInput } from \"../../coordination/resource.ts\";\nimport type { GrantToken } from \"../../coordination/types.ts\";\nimport type { RunAuthority } from \"./authority-runtime.ts\";\nimport { executeSandbox, type SandboxOutcome } from \"./broker.ts\";\nimport type { VerifiedRunJournal } from \"./journal.ts\";\nimport type { NamespaceIdentity } from \"./namespace-identity.ts\";\nimport { digestObject, VerifiedRunError } from \"./storage.ts\";\n\nexport interface OwnedRunCommand {\n\treadonly role: \"writer\" | \"verifier\";\n\treadonly argv: readonly string[];\n\treadonly workspace: string;\n\treadonly deadline: number;\n\treadonly claimId: string | null;\n\treadonly taskId?: string;\n}\nexport interface OwnedRunResult {\n\treadonly executionId: string;\n\treadonly result: SandboxOutcome;\n\treadonly timeoutMs: number;\n}\n\n/**\n * The filesystem claim a dispatched process is authorized for: exactly the\n * workspace bind it receives inside the namespace — nothing wider. Keys are\n * the resolved absolute path minus the leading slash so prefix containment\n * matches real tree containment; `realpathSync` keeps two spellings of the\n * same directory one claim.\n */\nfunction workspaceClaim(\n\tworkspace: string,\n\taccess: \"read\" | \"write\",\n\tgeneration: number,\n): readonly ResourceClaimInput[] {\n\tlet root: string;\n\ttry {\n\t\troot = realpathSync(workspace);\n\t} catch {\n\t\tthrow new VerifiedRunError(\"authority_claim\");\n\t}\n\treturn Object.freeze([\n\t\t{\n\t\t\tnamespace: \"filesystem\",\n\t\t\tinstanceId: \"verified-run\",\n\t\t\tcanonicalKey: root.replace(/^\\/+/, \"\"),\n\t\t\taccess,\n\t\t\tgeneration: String(generation),\n\t\t},\n\t]);\n}\n\n/** Rejection codes that can only be produced before the supervisor spawned. */\nconst NEVER_SPAWNED = new Set([\"cancelled\", \"deadline\", \"unsupported_boundary\"]);\n\n/**\n * Mandatory durable dispatch intent, shared by command and AgentSession\n * writers. There is no ungated execution lane: a journal without an anchored\n * budget cannot commit a process identity, so the supervisor boundary would\n * be unprovable and must refuse dispatch. Every dispatch now also crosses the\n * durable authority boundary — grant → intent → start → termination witness\n * — so a restart can reconcile effects the journal alone cannot settle.\n */\nexport async function executeRunCommand(\n\tjournal: VerifiedRunJournal,\n\trequest: OwnedRunCommand,\n\tpolicy: {\n\t\treadonly cleanupMs: number;\n\t\treadonly maxOutputBytes: number;\n\t\treadonly authority: RunAuthority;\n\t\treadonly signal?: AbortSignal;\n\t},\n): Promise<OwnedRunResult> {\n\tif (Math.floor(request.deadline - performance.now()) <= 0) throw new VerifiedRunError(\"deadline\");\n\tif (policy.signal?.aborted) throw new VerifiedRunError(\"cancelled\");\n\t// The supervisor boundary only exists once a budget (and therefore the\n\t// gated driver) is anchored. Dispatching without it would run code whose\n\t// termination cannot be witnessed — refuse rather than degrade.\n\tif (!journal.state.budget) throw new VerifiedRunError(\"unsupported_boundary\");\n\tconst authority = policy.authority;\n\tconst generation = journal.state.generation;\n\tconst executionId = randomUUID();\n\tconst claims = workspaceClaim(request.workspace, request.role === \"writer\" ? \"write\" : \"read\", generation);\n\tconst timeoutMs = Math.floor(request.deadline - performance.now());\n\tconst admission = authority.store.acquire({\n\t\tsessionId: authority.sessionId,\n\t\tincarnation: authority.incarnation,\n\t\tcommandId: executionId,\n\t\tintentDigest: digestObject({ argv: request.argv, claims, role: request.role }),\n\t\tclaims,\n\t\tnow: Date.now(),\n\t\tttl: timeoutMs + policy.cleanupMs + 30000,\n\t});\n\tif (admission.status !== \"granted\") {\n\t\t// A fresh executionId can only collide with a conflicting or wedged\n\t\t// claim — quarantined effects hold theirs until witnessed (fail closed).\n\t\tthrow new VerifiedRunError(\"authority_blocked\");\n\t}\n\tconst token: GrantToken = admission.token;\n\tlet supervisorInvoked = false;\n\tlet readyObserved = false;\n\tlet result: SandboxOutcome;\n\ttry {\n\t\tjournal.append({\n\t\t\tkind: \"dispatch\",\n\t\t\texecutionId,\n\t\t\trole: request.role,\n\t\t\tclaimId: request.claimId,\n\t\t\t...(request.taskId === undefined ? {} : { taskId: request.taskId }),\n\t\t});\n\t\tif (timeoutMs <= 0 || policy.signal?.aborted) {\n\t\t\tconst failure = policy.signal?.aborted ? \"cancelled\" : \"deadline\";\n\t\t\t// Provably pre-spawn: the intent never reached the adapter, so the grant\n\t\t\t// settles immediately instead of wedging as an unwitnessed live effect.\n\t\t\tauthority.store.cancel(token);\n\t\t\tauthority.store.confirmTerminated(token);\n\t\t\tjournal.append({ kind: \"exited\", executionId, failure });\n\t\t\tthrow new VerifiedRunError(failure);\n\t\t}\n\t\t// The intent commits before the adapter is invoked: from this record on,\n\t\t// the effect may be live and a crash must quarantine it, never assume\n\t\t// it did not run.\n\t\tif (!authority.store.dispatchIntent(token, executionId)) throw new VerifiedRunError(\"authority\");\n\t\tsupervisorInvoked = true;\n\t\tresult = await executeSandbox({\n\t\t\tworkspace: request.workspace,\n\t\t\targv: request.argv,\n\t\t\twritable: request.role === \"writer\",\n\t\t\ttimeoutMs: Math.floor(request.deadline - performance.now()),\n\t\t\tcleanupMs: policy.cleanupMs,\n\t\t\tmaxOutputBytes: policy.maxOutputBytes,\n\t\t\t...(policy.signal ? { signal: policy.signal } : {}),\n\t\t\tonReady: (identity: NamespaceIdentity) => {\n\t\t\t\treadyObserved = true;\n\t\t\t\tif (journal.state.generation !== generation) throw new VerifiedRunError(\"stale_generation\");\n\t\t\t\tjournal.append({ kind: \"process_ready\", executionId, identity });\n\t\t\t\tif (!authority.store.effectStarted(token, claims, identity)) throw new VerifiedRunError(\"authority\");\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\t// Only pre-spawn rejections prove nothing ran; everything else may have\n\t\t// a live or dying effect, which stays owned until a witness or a later\n\t\t// reconcile settles it.\n\t\ttry {\n\t\t\tauthority.store.cancel(token);\n\t\t\tif (\n\t\t\t\t!supervisorInvoked ||\n\t\t\t\t(!readyObserved && error instanceof VerifiedRunError && NEVER_SPAWNED.has(error.code))\n\t\t\t) {\n\t\t\t\tauthority.store.confirmTerminated(token);\n\t\t\t\t// The intent provably never reached a process: journal the exit the same\n\t\t\t\t// way the pre-spawn timeout branch does, or the run quarantines forever\n\t\t\t\t// on an open executionId — the slow-runner flake where the second\n\t\t\t\t// deadline evaluation crosses zero between dispatch and spawn.\n\t\t\t\tif (journal.state.activeExecutionIds.includes(executionId)) {\n\t\t\t\t\tjournal.append({\n\t\t\t\t\t\tkind: \"exited\",\n\t\t\t\t\t\texecutionId,\n\t\t\t\t\t\tfailure: error instanceof VerifiedRunError ? error.code : \"execution_failed\",\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (cleanupError) {\n\t\t\tthrow new AggregateError([error, cleanupError], \"Execution ownership cleanup failed\");\n\t\t}\n\t\tthrow error;\n\t}\n\tif (result.failure === \"cancelled\") authority.store.cancel(token);\n\t// Resolve means both witnesses fired: the direct child closed and the\n\t// recorded namespace drained empty. That is the termination-observed event\n\t// — the grant releases its claims here and nowhere else.\n\tif (!authority.store.confirmTerminated(token)) throw new VerifiedRunError(\"authority\");\n\tif (journal.state.generation !== generation) throw new VerifiedRunError(\"stale_generation\");\n\tjournal.append({ kind: \"exited\", executionId, failure: request.role === \"writer\" ? result.failure : null });\n\treturn { executionId, result, timeoutMs };\n}\n"]}
@@ -72,6 +72,10 @@ export declare function probeOwnedNamespace(identity: NamespaceIdentity): "termi
72
72
  * probe is inconclusive, and that window resolves to `gone` once the kernel
73
73
  * finishes teardown. Only a probe that is still inconclusive at the deadline
74
74
  * stays `unknown`; a still-alive init is `populated`.
75
+ *
76
+ * `drained` means the kernel has provably killed every member; a task frozen
77
+ * in uninterruptible kernel sleep can linger in /proc briefly afterwards —
78
+ * that is kernel bookkeeping, not a live boundary.
75
79
  */
76
80
  export declare function awaitBoundaryDrained(identity: NamespaceIdentity, budgetMs: number): Promise<"drained" | "populated" | "unknown">;
77
81
  //# sourceMappingURL=supervisor-adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/verified-run/supervisor-adapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,iBAAiB,EAAkB,MAAM,yBAAyB,CAAC;AAGjF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AAElD,eAAO,MAAM,sBAAsB,iWA+BzB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,OAAO,CAAA;CAAE,CAAC;AAErF;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAIrG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAEhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,GAAG,MAAM,EAAE,CAiC5F;AAED,iGAAiG;AACjG,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,CAGnG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACzC,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC,CAQ9C","sourcesContent":["import { existsSync, readdirSync, readFileSync, readlinkSync, statSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { type NamespaceIdentity, probeNamespace } from \"./namespace-identity.ts\";\nimport { VerifiedRunError } from \"./storage.ts\";\n\n/**\n * Owned-process supervisor boundary (WP02).\n *\n * Three concerns are separated here so the broker only orchestrates them:\n *\n * - spawn: `SUPERVISOR_SYSTEM_ARGS` + `loadSupervisorBackend` describe the only\n * backend this package trusts — bubblewrap with `--unshare-all`, which puts\n * the supervised command in a fresh PID namespace whose init is the only\n * process the host can see. A platform without that backend has no way to\n * prove termination, so callers must refuse with `unsupported_boundary`\n * instead of degrading to best-effort signaling.\n * - cancel: `escalateTermination` sends SIGKILL to the supervised direct\n * child (the bwrap supervisor). `--die-with-parent` propagates death to the\n * namespace init, and the kernel then SIGKILLs every remaining task in that\n * namespace — including non-cooperative workers and detached (`setsid`)\n * descendants. Signal delivery is never treated as termination.\n * - termination witness: `probeNamespace` detects the recorded namespace\n * init's death and `awaitBoundaryDrained` waits for it. When a PID\n * namespace's init exits, the kernel SIGKILLs every remaining task in that\n * namespace, so init death is the exact termination witness — a `close`\n * event, a resolved promise, or a dead direct child with surviving\n * descendants is not. `namespaceMemberPids` remains as a diagnostic\n * enumeration only: host process-table scans are environment-dependent\n * (zombies keep their ns link until reaped; foreign same-uid tasks can be\n * unreadable) and have poisoned drains on kernels where either occurs.\n */\nexport const SUPERVISOR_BINARY = \"/usr/bin/bwrap\";\n\nexport const SUPERVISOR_SYSTEM_ARGS = [\n\t\"--unshare-all\",\n\t\"--die-with-parent\",\n\t\"--new-session\",\n\t\"--cap-drop\",\n\t\"ALL\",\n\t\"--clearenv\",\n\t\"--ro-bind\",\n\t\"/usr\",\n\t\"/usr\",\n\t\"--symlink\",\n\t\"usr/bin\",\n\t\"/bin\",\n\t\"--symlink\",\n\t\"usr/lib\",\n\t\"/lib\",\n\t\"--symlink\",\n\t\"usr/lib64\",\n\t\"/lib64\",\n\t\"--proc\",\n\t\"/proc\",\n\t\"--dev\",\n\t\"/dev\",\n\t\"--tmpfs\",\n\t\"/tmp\",\n\t\"--setenv\",\n\t\"PATH\",\n\t\"/usr/bin:/bin\",\n\t\"--setenv\",\n\t\"LANG\",\n\t\"C.UTF-8\",\n] as const;\n\nexport type SupervisorChild = { readonly kill: (signal: NodeJS.Signals) => boolean };\n\n/**\n * The only termination guarantee this package honors: a private PID namespace\n * plus the Linux bwrap supervisor. Anything else is refused at the boundary.\n */\nexport function loadSupervisorBackend(): { readonly binary: string; readonly args: readonly string[] } {\n\tif (process.platform !== \"linux\" || !existsSync(SUPERVISOR_BINARY))\n\t\tthrow new VerifiedRunError(\"unsupported_boundary\");\n\treturn { binary: SUPERVISOR_BINARY, args: SUPERVISOR_SYSTEM_ARGS };\n}\n\n/**\n * SIGKILL escalation to the supervised direct child. `--die-with-parent` makes\n * the kernel kill the whole PID namespace once this supervisor dies, so no\n * per-descendant signaling (which PID reuse would make unsafe) is needed.\n */\nexport function escalateTermination(child: SupervisorChild): void {\n\tchild.kill(\"SIGKILL\");\n}\n\n/**\n * Enumerate host PIDs of every task still inside `identity.namespace`.\n *\n * Membership is keyed on the recorded namespace symlink, not on the init's\n * PID: a fabricated or reaped direct child cannot mask surviving descendants.\n *\n * A task whose namespace link cannot be read is only a candidate member when\n * it could belong to the recorded boundary at all. The backend's members\n * always run under our real uid — the pid namespace is owned by a user\n * namespace we created and `CAP_SETUID` is dropped — so a task owned by\n * another uid, or one that vanished mid-scan, cannot be a member and is\n * skipped. An unreadable task owned by our uid is conservatively counted:\n * failing to prove emptiness must fail closed.\n */\nexport function namespaceMemberPids(identity: Pick<NamespaceIdentity, \"namespace\">): number[] {\n\tconst members: number[] = [];\n\t// A platform without getuid cannot exclude foreign-owned tasks, so every\n\t// unreadable entry stays a candidate member (fail closed).\n\tconst uid = typeof process.getuid === \"function\" ? process.getuid() : undefined;\n\tfor (const entry of readdirSync(\"/proc\")) {\n\t\tif (!/^\\d+$/.test(entry)) continue;\n\t\ttry {\n\t\t\tif (readlinkSync(`/proc/${entry}/ns/pid`) !== identity.namespace) continue;\n\t\t\t// A zombie still carries the namespace link until reaped, but it\n\t\t\t// cannot run: counting it as a member confuses teardown with\n\t\t\t// survival.\n\t\t\tconst stat = readFileSync(`/proc/${entry}/stat`, \"utf8\");\n\t\t\tconst state = stat\n\t\t\t\t.slice(stat.lastIndexOf(\")\") + 2)\n\t\t\t\t.trim()\n\t\t\t\t.split(/\\s+/)[0];\n\t\t\tif (state !== \"Z\" && state !== \"X\") members.push(Number(entry));\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error && \"code\" in error)) continue;\n\t\t\tif (error.code === \"ENOENT\" || error.code === \"ESRCH\") continue;\n\t\t\ttry {\n\t\t\t\tif (uid === undefined || statSync(`/proc/${entry}`).uid === uid) members.push(Number(entry));\n\t\t\t} catch (statError) {\n\t\t\t\tif (\n\t\t\t\t\t!(statError instanceof Error && \"code\" in statError) ||\n\t\t\t\t\t(statError.code !== \"ENOENT\" && statError.code !== \"ESRCH\")\n\t\t\t\t)\n\t\t\t\t\tmembers.push(Number(entry));\n\t\t\t}\n\t\t}\n\t}\n\treturn members;\n}\n\n/** Init death terminates the whole namespace; a zombie init already proves teardown finished. */\nexport function probeOwnedNamespace(identity: NamespaceIdentity): \"terminated\" | \"alive\" | \"unknown\" {\n\tconst status = probeNamespace(identity);\n\treturn status === \"gone\" ? \"terminated\" : status;\n}\n\n/**\n * Wait for the namespace init's death within budgetMs. Init death is the\n * termination witness: the kernel SIGKILLs every remaining task in the PID\n * namespace when its init exits, so a drained namespace needs no process-table\n * enumeration. `populated`/`unknown` never release claims.\n *\n * `unknown` is not terminal here: teardown has a transient window where the\n * init's stat is readable but its ns link read is denied or the identity\n * probe is inconclusive, and that window resolves to `gone` once the kernel\n * finishes teardown. Only a probe that is still inconclusive at the deadline\n * stays `unknown`; a still-alive init is `populated`.\n */\nexport async function awaitBoundaryDrained(\n\tidentity: NamespaceIdentity,\n\tbudgetMs: number,\n): Promise<\"drained\" | \"populated\" | \"unknown\"> {\n\tconst deadline = performance.now() + Math.max(0, budgetMs);\n\tfor (;;) {\n\t\tconst status = probeNamespace(identity);\n\t\tif (status === \"gone\") return \"drained\";\n\t\tif (performance.now() >= deadline) return status === \"alive\" ? \"populated\" : \"unknown\";\n\t\tawait delay(Math.min(10, Math.max(1, deadline - performance.now())));\n\t}\n}\n"]}
1
+ {"version":3,"file":"supervisor-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/verified-run/supervisor-adapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,iBAAiB,EAAkB,MAAM,yBAAyB,CAAC;AAGjF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AAElD,eAAO,MAAM,sBAAsB,iWA+BzB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,OAAO,CAAA;CAAE,CAAC;AAErF;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAIrG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAEhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,GAAG,MAAM,EAAE,CAiC5F;AAED,iGAAiG;AACjG,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,CAGnG;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,oBAAoB,CACzC,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC,CAQ9C","sourcesContent":["import { existsSync, readdirSync, readFileSync, readlinkSync, statSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { type NamespaceIdentity, probeNamespace } from \"./namespace-identity.ts\";\nimport { VerifiedRunError } from \"./storage.ts\";\n\n/**\n * Owned-process supervisor boundary (WP02).\n *\n * Three concerns are separated here so the broker only orchestrates them:\n *\n * - spawn: `SUPERVISOR_SYSTEM_ARGS` + `loadSupervisorBackend` describe the only\n * backend this package trusts — bubblewrap with `--unshare-all`, which puts\n * the supervised command in a fresh PID namespace whose init is the only\n * process the host can see. A platform without that backend has no way to\n * prove termination, so callers must refuse with `unsupported_boundary`\n * instead of degrading to best-effort signaling.\n * - cancel: `escalateTermination` sends SIGKILL to the supervised direct\n * child (the bwrap supervisor). `--die-with-parent` propagates death to the\n * namespace init, and the kernel then SIGKILLs every remaining task in that\n * namespace — including non-cooperative workers and detached (`setsid`)\n * descendants. Signal delivery is never treated as termination.\n * - termination witness: `probeNamespace` detects the recorded namespace\n * init's death and `awaitBoundaryDrained` waits for it. When a PID\n * namespace's init exits, the kernel SIGKILLs every remaining task in that\n * namespace, so init death is the exact termination witness — a `close`\n * event, a resolved promise, or a dead direct child with surviving\n * descendants is not. `namespaceMemberPids` remains as a diagnostic\n * enumeration only: host process-table scans are environment-dependent\n * (zombies keep their ns link until reaped; foreign same-uid tasks can be\n * unreadable) and have poisoned drains on kernels where either occurs.\n */\nexport const SUPERVISOR_BINARY = \"/usr/bin/bwrap\";\n\nexport const SUPERVISOR_SYSTEM_ARGS = [\n\t\"--unshare-all\",\n\t\"--die-with-parent\",\n\t\"--new-session\",\n\t\"--cap-drop\",\n\t\"ALL\",\n\t\"--clearenv\",\n\t\"--ro-bind\",\n\t\"/usr\",\n\t\"/usr\",\n\t\"--symlink\",\n\t\"usr/bin\",\n\t\"/bin\",\n\t\"--symlink\",\n\t\"usr/lib\",\n\t\"/lib\",\n\t\"--symlink\",\n\t\"usr/lib64\",\n\t\"/lib64\",\n\t\"--proc\",\n\t\"/proc\",\n\t\"--dev\",\n\t\"/dev\",\n\t\"--tmpfs\",\n\t\"/tmp\",\n\t\"--setenv\",\n\t\"PATH\",\n\t\"/usr/bin:/bin\",\n\t\"--setenv\",\n\t\"LANG\",\n\t\"C.UTF-8\",\n] as const;\n\nexport type SupervisorChild = { readonly kill: (signal: NodeJS.Signals) => boolean };\n\n/**\n * The only termination guarantee this package honors: a private PID namespace\n * plus the Linux bwrap supervisor. Anything else is refused at the boundary.\n */\nexport function loadSupervisorBackend(): { readonly binary: string; readonly args: readonly string[] } {\n\tif (process.platform !== \"linux\" || !existsSync(SUPERVISOR_BINARY))\n\t\tthrow new VerifiedRunError(\"unsupported_boundary\");\n\treturn { binary: SUPERVISOR_BINARY, args: SUPERVISOR_SYSTEM_ARGS };\n}\n\n/**\n * SIGKILL escalation to the supervised direct child. `--die-with-parent` makes\n * the kernel kill the whole PID namespace once this supervisor dies, so no\n * per-descendant signaling (which PID reuse would make unsafe) is needed.\n */\nexport function escalateTermination(child: SupervisorChild): void {\n\tchild.kill(\"SIGKILL\");\n}\n\n/**\n * Enumerate host PIDs of every task still inside `identity.namespace`.\n *\n * Membership is keyed on the recorded namespace symlink, not on the init's\n * PID: a fabricated or reaped direct child cannot mask surviving descendants.\n *\n * A task whose namespace link cannot be read is only a candidate member when\n * it could belong to the recorded boundary at all. The backend's members\n * always run under our real uid — the pid namespace is owned by a user\n * namespace we created and `CAP_SETUID` is dropped — so a task owned by\n * another uid, or one that vanished mid-scan, cannot be a member and is\n * skipped. An unreadable task owned by our uid is conservatively counted:\n * failing to prove emptiness must fail closed.\n */\nexport function namespaceMemberPids(identity: Pick<NamespaceIdentity, \"namespace\">): number[] {\n\tconst members: number[] = [];\n\t// A platform without getuid cannot exclude foreign-owned tasks, so every\n\t// unreadable entry stays a candidate member (fail closed).\n\tconst uid = typeof process.getuid === \"function\" ? process.getuid() : undefined;\n\tfor (const entry of readdirSync(\"/proc\")) {\n\t\tif (!/^\\d+$/.test(entry)) continue;\n\t\ttry {\n\t\t\tif (readlinkSync(`/proc/${entry}/ns/pid`) !== identity.namespace) continue;\n\t\t\t// A zombie still carries the namespace link until reaped, but it\n\t\t\t// cannot run: counting it as a member confuses teardown with\n\t\t\t// survival.\n\t\t\tconst stat = readFileSync(`/proc/${entry}/stat`, \"utf8\");\n\t\t\tconst state = stat\n\t\t\t\t.slice(stat.lastIndexOf(\")\") + 2)\n\t\t\t\t.trim()\n\t\t\t\t.split(/\\s+/)[0];\n\t\t\tif (state !== \"Z\" && state !== \"X\") members.push(Number(entry));\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error && \"code\" in error)) continue;\n\t\t\tif (error.code === \"ENOENT\" || error.code === \"ESRCH\") continue;\n\t\t\ttry {\n\t\t\t\tif (uid === undefined || statSync(`/proc/${entry}`).uid === uid) members.push(Number(entry));\n\t\t\t} catch (statError) {\n\t\t\t\tif (\n\t\t\t\t\t!(statError instanceof Error && \"code\" in statError) ||\n\t\t\t\t\t(statError.code !== \"ENOENT\" && statError.code !== \"ESRCH\")\n\t\t\t\t)\n\t\t\t\t\tmembers.push(Number(entry));\n\t\t\t}\n\t\t}\n\t}\n\treturn members;\n}\n\n/** Init death terminates the whole namespace; a zombie init already proves teardown finished. */\nexport function probeOwnedNamespace(identity: NamespaceIdentity): \"terminated\" | \"alive\" | \"unknown\" {\n\tconst status = probeNamespace(identity);\n\treturn status === \"gone\" ? \"terminated\" : status;\n}\n\n/**\n * Wait for the namespace init's death within budgetMs. Init death is the\n * termination witness: the kernel SIGKILLs every remaining task in the PID\n * namespace when its init exits, so a drained namespace needs no process-table\n * enumeration. `populated`/`unknown` never release claims.\n *\n * `unknown` is not terminal here: teardown has a transient window where the\n * init's stat is readable but its ns link read is denied or the identity\n * probe is inconclusive, and that window resolves to `gone` once the kernel\n * finishes teardown. Only a probe that is still inconclusive at the deadline\n * stays `unknown`; a still-alive init is `populated`.\n *\n * `drained` means the kernel has provably killed every member; a task frozen\n * in uninterruptible kernel sleep can linger in /proc briefly afterwards —\n * that is kernel bookkeeping, not a live boundary.\n */\nexport async function awaitBoundaryDrained(\n\tidentity: NamespaceIdentity,\n\tbudgetMs: number,\n): Promise<\"drained\" | \"populated\" | \"unknown\"> {\n\tconst deadline = performance.now() + Math.max(0, budgetMs);\n\tfor (;;) {\n\t\tconst status = probeNamespace(identity);\n\t\tif (status === \"gone\") return \"drained\";\n\t\tif (performance.now() >= deadline) return status === \"alive\" ? \"populated\" : \"unknown\";\n\t\tawait delay(Math.min(10, Math.max(1, deadline - performance.now())));\n\t}\n}\n"]}
@@ -149,6 +149,10 @@ export function probeOwnedNamespace(identity) {
149
149
  * probe is inconclusive, and that window resolves to `gone` once the kernel
150
150
  * finishes teardown. Only a probe that is still inconclusive at the deadline
151
151
  * stays `unknown`; a still-alive init is `populated`.
152
+ *
153
+ * `drained` means the kernel has provably killed every member; a task frozen
154
+ * in uninterruptible kernel sleep can linger in /proc briefly afterwards —
155
+ * that is kernel bookkeeping, not a live boundary.
152
156
  */
153
157
  export async function awaitBoundaryDrained(identity, budgetMs) {
154
158
  const deadline = performance.now() + Math.max(0, budgetMs);