@gajae-code/coding-agent 0.18.6 → 0.18.8

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 (302) hide show
  1. package/CHANGELOG.md +318 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +6 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/settings.d.ts +4 -0
  15. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  16. package/dist/types/cursor-delete-path.d.ts +1 -0
  17. package/dist/types/cursor-mcp-name.d.ts +8 -0
  18. package/dist/types/customization/inventory.d.ts +2 -0
  19. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  22. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  23. package/dist/types/discovery/helpers.d.ts +8 -3
  24. package/dist/types/export/html/css-color.d.ts +2 -0
  25. package/dist/types/export/html/template.generated.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  27. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  28. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  29. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  31. package/dist/types/extensibility/skill-management.d.ts +3 -1
  32. package/dist/types/extensibility/skills.d.ts +2 -0
  33. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  34. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  35. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  36. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  37. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  38. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  39. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  40. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  42. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  43. package/dist/types/lsp/workspace-path.d.ts +1 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  46. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  47. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  48. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  49. package/dist/types/modes/interactive-mode.d.ts +1 -0
  50. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  51. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  52. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  53. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  54. package/dist/types/progress/progress-contract.d.ts +154 -0
  55. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  56. package/dist/types/progress/project-progress.d.ts +141 -0
  57. package/dist/types/progress/render-progress.d.ts +28 -0
  58. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  59. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  60. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  61. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  62. package/dist/types/runtime-mcp/types.d.ts +6 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  64. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  65. package/dist/types/sdk/broker/broker.d.ts +18 -1
  66. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  67. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  68. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  69. package/dist/types/sdk/bus/index.d.ts +12 -1
  70. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  71. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/host/host.d.ts +6 -0
  74. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  75. package/dist/types/sdk/index.d.ts +1 -0
  76. package/dist/types/sdk/router/session-router.d.ts +9 -0
  77. package/dist/types/sdk/session-list.d.ts +1 -1
  78. package/dist/types/sdk/session.d.ts +8 -0
  79. package/dist/types/session/agent-session.d.ts +38 -9
  80. package/dist/types/session/artifacts.d.ts +2 -2
  81. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  82. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  83. package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
  84. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  85. package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
  86. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
  87. package/dist/types/session/session-manager.d.ts +31 -5
  88. package/dist/types/session/session-retirement.d.ts +71 -0
  89. package/dist/types/session/session-storage.d.ts +62 -48
  90. package/dist/types/session/session-transcript-header.d.ts +2 -0
  91. package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
  92. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  93. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  94. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  95. package/dist/types/setup/credential-import.d.ts +6 -4
  96. package/dist/types/skill-state/active-state.d.ts +38 -0
  97. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  98. package/dist/types/task/executor.d.ts +3 -0
  99. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  100. package/dist/types/tools/index.d.ts +3 -0
  101. package/dist/types/tools/python.d.ts +9 -1
  102. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  103. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  104. package/dist/types/tools/todo-write.d.ts +6 -0
  105. package/dist/types/utils/tools-checksum.d.ts +7 -0
  106. package/package.json +7 -7
  107. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  108. package/scripts/resident-memory-bench.ts +174 -20
  109. package/scripts/verify-managed-empty-stop.ts +28 -0
  110. package/src/async/job-manager.ts +228 -122
  111. package/src/capability/index.ts +18 -0
  112. package/src/capability/mcp.ts +2 -0
  113. package/src/capability/skill.ts +2 -0
  114. package/src/capability/types.ts +6 -0
  115. package/src/cli/auth-broker-cli.ts +1 -1
  116. package/src/cli/config-cli.ts +8 -18
  117. package/src/cli/customize-doctor.ts +15 -2
  118. package/src/cli/public-command-errors.ts +21 -1
  119. package/src/cli/setup-cli.ts +4 -4
  120. package/src/cli/skills-cli.ts +5 -0
  121. package/src/cli-main.ts +7 -8
  122. package/src/commands/deep-interview.ts +47 -8
  123. package/src/commands/sdk.ts +140 -29
  124. package/src/config/autorouting-tier-map.ts +117 -7
  125. package/src/config/file-lock.ts +307 -68
  126. package/src/config/mcp-schema.json +4 -0
  127. package/src/config/model-bindings-applier.ts +20 -0
  128. package/src/config/model-profile-activation.ts +240 -46
  129. package/src/config/model-registry.ts +47 -153
  130. package/src/config/settings.ts +44 -4
  131. package/src/config/skill-settings-defaults.ts +7 -0
  132. package/src/coordinator-mcp/policy.ts +3 -2
  133. package/src/cursor-delete-path.ts +10 -0
  134. package/src/cursor-mcp-name.ts +15 -0
  135. package/src/cursor.ts +13 -2
  136. package/src/customization/inventory.ts +3 -0
  137. package/src/debug/report-bundle-parent.ts +22 -0
  138. package/src/debug/report-bundle.ts +3 -0
  139. package/src/debug/system-info.ts +13 -1
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  141. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  142. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  143. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  144. package/src/defaults/gjc-defaults.test.ts +18 -1
  145. package/src/defaults/gjc-defaults.ts +3 -0
  146. package/src/defaults/gjc-skills.generated.ts +58 -36
  147. package/src/discovery/agents.ts +6 -1
  148. package/src/discovery/builtin.ts +10 -0
  149. package/src/discovery/helpers.ts +168 -23
  150. package/src/discovery/mcp-json.ts +6 -0
  151. package/src/discovery/ssh.ts +9 -4
  152. package/src/discovery/substitute-plugin-root.ts +3 -1
  153. package/src/edit/file-read-cache.ts +1 -1
  154. package/src/eval/py/executor.ts +380 -120
  155. package/src/export/html/css-color.ts +9 -0
  156. package/src/export/html/index.ts +5 -4
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/extensibility/extensions/runner.ts +4 -0
  159. package/src/extensibility/extensions/types.ts +7 -0
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  161. package/src/extensibility/plugins/manager.ts +3 -2
  162. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  163. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  164. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  165. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  166. package/src/extensibility/skill-management.ts +6 -2
  167. package/src/extensibility/skills.ts +6 -1
  168. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  169. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  170. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  171. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  172. package/src/gjc-runtime/gc-runtime.ts +206 -8
  173. package/src/gjc-runtime/launch-worktree.ts +14 -4
  174. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  175. package/src/gjc-runtime/state-argv.ts +2 -0
  176. package/src/gjc-runtime/state-migrations.ts +91 -29
  177. package/src/gjc-runtime/state-runtime.ts +3716 -135
  178. package/src/gjc-runtime/state-writer.ts +83 -8
  179. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  180. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  181. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  182. package/src/harness-control-plane/operate.ts +12 -4
  183. package/src/harness-control-plane/owner.ts +5 -1
  184. package/src/hashline/parser.ts +48 -9
  185. package/src/hashline/recovery.ts +13 -4
  186. package/src/hindsight/config.ts +9 -3
  187. package/src/internal-urls/docs-index.generated.ts +19 -18
  188. package/src/internal-urls/skill-path.ts +19 -0
  189. package/src/internal-urls/skill-protocol.ts +2 -0
  190. package/src/lsp/edits.ts +6 -0
  191. package/src/lsp/workspace-path.ts +51 -0
  192. package/src/main.ts +195 -31
  193. package/src/modes/acp/acp-agent.ts +163 -7
  194. package/src/modes/components/custom-editor.ts +14 -0
  195. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  196. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  197. package/src/modes/components/extensions/state-manager.ts +15 -5
  198. package/src/modes/components/model-selector.ts +1 -4
  199. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  200. package/src/modes/components/status-line/segments.ts +4 -0
  201. package/src/modes/controllers/event-controller.ts +11 -4
  202. package/src/modes/controllers/input-controller.ts +74 -29
  203. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  204. package/src/modes/controllers/selector-controller.ts +28 -15
  205. package/src/modes/interactive-mode.ts +4 -0
  206. package/src/modes/runtime-init.ts +4 -0
  207. package/src/modes/utils/abort-message.ts +3 -1
  208. package/src/modes/utils/interrupt-ownership.ts +12 -0
  209. package/src/modes/utils/keybinding-matchers.ts +4 -0
  210. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  211. package/src/progress/collect-project-progress.ts +153 -0
  212. package/src/progress/progress-contract.ts +172 -0
  213. package/src/progress/progress-snapshot.ts +140 -0
  214. package/src/progress/project-progress.ts +446 -0
  215. package/src/progress/render-progress.ts +209 -0
  216. package/src/prompts/system/master-mode.md +1 -0
  217. package/src/prompts/tools/skill-discovery.md +1 -1
  218. package/src/runtime-mcp/client.ts +13 -4
  219. package/src/runtime-mcp/config-writer.ts +10 -6
  220. package/src/runtime-mcp/config.ts +1 -0
  221. package/src/runtime-mcp/elicitation.ts +142 -0
  222. package/src/runtime-mcp/oauth-flow.ts +5 -0
  223. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  224. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  225. package/src/runtime-mcp/smithery-registry.ts +22 -3
  226. package/src/runtime-mcp/transports/http.ts +2 -2
  227. package/src/runtime-mcp/types.ts +6 -0
  228. package/src/sdk/acp/adapter.ts +93 -8
  229. package/src/sdk/broker/broker-exit.ts +52 -23
  230. package/src/sdk/broker/broker.ts +83 -7
  231. package/src/sdk/broker/ensure.ts +2 -0
  232. package/src/sdk/broker/lifecycle.ts +820 -73
  233. package/src/sdk/broker/session-index.ts +191 -81
  234. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  235. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  236. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  238. package/src/sdk/bus/index.ts +241 -46
  239. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  240. package/src/sdk/bus/telegram-daemon.ts +4 -2
  241. package/src/sdk/cli/session-cli.ts +17 -3
  242. package/src/sdk/client/client.ts +28 -8
  243. package/src/sdk/host/host.ts +6 -0
  244. package/src/sdk/host/query/handlers.ts +9 -1
  245. package/src/sdk/host/session-runtime.test.ts +2805 -411
  246. package/src/sdk/host/session-runtime.ts +428 -111
  247. package/src/sdk/host/surface-policy.ts +1 -0
  248. package/src/sdk/index.ts +8 -0
  249. package/src/sdk/lifecycle-session.ts +3 -1
  250. package/src/sdk/mcp/server.ts +15 -3
  251. package/src/sdk/prompt-deadline-manager.ts +15 -0
  252. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  253. package/src/sdk/protocol/operation-registry.ts +9 -2
  254. package/src/sdk/router/session-router.ts +94 -14
  255. package/src/sdk/session-list.ts +1 -0
  256. package/src/sdk/session.ts +81 -38
  257. package/src/session/agent-session.ts +674 -224
  258. package/src/session/artifacts.ts +174 -70
  259. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  260. package/src/session/internal/managed-session-scope.ts +3457 -462
  261. package/src/session/internal/managed-session-storage.ts +482 -61
  262. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  263. package/src/session/internal/task-artifact-owner-access.ts +245 -0
  264. package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
  265. package/src/session/session-manager.ts +939 -133
  266. package/src/session/session-retirement.ts +714 -0
  267. package/src/session/session-storage.ts +418 -174
  268. package/src/session/session-transcript-header.ts +17 -0
  269. package/src/session/task-artifact-owner-codec.ts +641 -0
  270. package/src/session/task-artifact-owner-retirement.ts +477 -0
  271. package/src/session/task-artifact-owner.ts +289 -0
  272. package/src/setup/credential-auto-import.ts +12 -3
  273. package/src/setup/credential-import.ts +176 -5
  274. package/src/setup/paseo/announce.ts +6 -1
  275. package/src/skill-state/active-state.ts +132 -2
  276. package/src/slash-commands/builtin-registry.ts +39 -9
  277. package/src/slash-commands/helpers/progress-report.ts +23 -0
  278. package/src/stt/setup.ts +3 -5
  279. package/src/task/commands.ts +3 -1
  280. package/src/task/executor.ts +4 -0
  281. package/src/task/index.ts +6 -0
  282. package/src/tools/ask.ts +205 -6
  283. package/src/tools/ast-edit.ts +4 -0
  284. package/src/tools/bash-allowed-prefixes.ts +15 -0
  285. package/src/tools/bash-skill-urls.ts +56 -3
  286. package/src/tools/bash.ts +48 -79
  287. package/src/tools/descriptors.ts +5 -1
  288. package/src/tools/eval.ts +56 -36
  289. package/src/tools/image-gen.ts +1 -1
  290. package/src/tools/index.ts +3 -0
  291. package/src/tools/python.ts +187 -66
  292. package/src/tools/recipe/index.ts +9 -0
  293. package/src/tools/recipe/recipe-guard.ts +19 -0
  294. package/src/tools/restricted-cwd.ts +10 -0
  295. package/src/tools/skill-discovery.ts +5 -0
  296. package/src/tools/skill.ts +28 -1
  297. package/src/tools/todo-write.ts +10 -1
  298. package/src/tools/tool-catalog.generated.ts +1 -1
  299. package/src/tools/write.ts +1 -0
  300. package/src/utils/tools-checksum.ts +34 -0
  301. package/src/utils/tools-manager.ts +24 -6
  302. package/src/web/search/providers/codex.ts +3 -2
@@ -13,7 +13,7 @@ function nativeLifecycle(): typeof import("@gajae-code/natives") {
13
13
  return nativeLifecycleBindings;
14
14
  }
15
15
 
16
- import { $credentialEnv, logger, resolveEquivalentPath } from "@gajae-code/utils";
16
+ import { $credentialEnv, getSessionsDir, logger, resolveEquivalentPath } from "@gajae-code/utils";
17
17
  import {
18
18
  loadAcceptedModelPresetRegistry,
19
19
  loadAcceptedModelPresetRegistryAsync,
@@ -43,7 +43,19 @@ import {
43
43
  GJC_COORDINATOR_SIDECAR_SIGNATURE_REQUIRED_ENV,
44
44
  GJC_COORDINATOR_SIDECAR_SIGNING_KEY_ENV,
45
45
  } from "../../gjc-runtime/session-state-sidecar";
46
+ import {
47
+ type ManagedGcProtocolScopeInspector,
48
+ type ManagedScope,
49
+ managedGcProtocolScopeInspectorForScope,
50
+ resolveManagedGcScopeForRead,
51
+ taskArtifactOwnerStorageContextForScope,
52
+ } from "../../session/internal/managed-session-scope";
46
53
  import { validateManagedArtifactTree } from "../../session/internal/managed-session-storage";
54
+ import { captureTaskArtifactOwnerDeletionEvidence } from "../../session/internal/task-artifact-owner-access";
55
+ import {
56
+ hasSiblingTaskArtifactOwnerTranscript,
57
+ taskArtifactOwnerLocatorFromTranscriptBytes,
58
+ } from "../../session/internal/task-artifact-owner-transcript";
47
59
  import {
48
60
  FileSessionStorage,
49
61
  SessionDeleteVerificationError,
@@ -52,7 +64,18 @@ import {
52
64
  type VerifiedSessionDeleteResult,
53
65
  type VerifiedSessionDeleteTarget,
54
66
  } from "../../session/session-storage";
67
+ import { parseFirstJsonlLine } from "../../session/session-transcript-header";
55
68
  import type { SessionWorkLease } from "../../session/session-work-lease";
69
+ import type {
70
+ TaskArtifactOwnerDeletionEvidence,
71
+ TaskArtifactOwnerLocator,
72
+ TaskArtifactOwnerRetirementOutcome,
73
+ TaskArtifactOwnerStorageContext,
74
+ } from "../../session/task-artifact-owner-codec";
75
+ import {
76
+ retireTaskArtifactOwner,
77
+ verifyTaskArtifactOwnerPhysicalRetirement,
78
+ } from "../../session/task-artifact-owner-retirement";
56
79
  import type { SessionLifecycleMcpServer } from "../acp/mcp";
57
80
  import { SdkClient, SdkClientError } from "../client/client";
58
81
  import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "../host/control/runtime-gate";
@@ -106,6 +129,12 @@ import {
106
129
  readPreparationTimeouts,
107
130
  startupQueueWaitMs,
108
131
  } from "./startup-budget";
132
+ import {
133
+ type BrokerTaskArtifactOwnerCleanupValidation,
134
+ type BrokerTaskArtifactOwnerRetirementDisposition,
135
+ decodeBrokerTaskArtifactOwnerCleanupFields,
136
+ serializeBrokerTaskArtifactOwnerRetirementDisposition,
137
+ } from "./task-artifact-owner-validation";
109
138
  import { worktreeOccupant } from "./worktree-occupancy";
110
139
 
111
140
  export {
@@ -961,6 +990,130 @@ function lifecycleLaunchKnownSecrets(launch: SessionLaunch): string[] {
961
990
  }
962
991
 
963
992
  type CleanupEvidence = BrokerCleanupEvidence;
993
+
994
+ function brokerTaskArtifactOwnerCleanupFields(cleanup: CleanupEvidence): Record<string, unknown> {
995
+ const fields: Record<string, unknown> = {};
996
+ for (const [key, value] of Object.entries(cleanup)) {
997
+ if (key.startsWith("taskArtifactOwner")) fields[key] = value;
998
+ }
999
+ return fields;
1000
+ }
1001
+
1002
+ function managedOwnerScopeFromInventory(scope: ManagedSessionScope): ManagedScope {
1003
+ const resolved = resolveManagedGcScopeForRead({
1004
+ cwd: scope.legacyLexicalCwd,
1005
+ agentDir: scope.agentDir,
1006
+ sessionsRoot: scope.sessionsRoot,
1007
+ });
1008
+ if (
1009
+ resolved.kind !== "resolved" ||
1010
+ resolved.scope.apiVersion !== scope.apiVersion ||
1011
+ resolved.scope.layoutVersion !== scope.layoutVersion ||
1012
+ resolved.scope.identityVersion !== scope.identityVersion ||
1013
+ resolved.scope.agentDir !== scope.agentDir ||
1014
+ resolved.scope.sessionsRoot !== scope.sessionsRoot ||
1015
+ resolved.scope.canonicalCwd !== scope.canonicalCwd ||
1016
+ resolved.scope.legacyLexicalCwd !== scope.legacyLexicalCwd ||
1017
+ resolved.scope.directoryName !== scope.directoryName ||
1018
+ resolved.scope.directoryPath !== scope.directoryPath
1019
+ )
1020
+ throw new Error("managed_task_artifact_owner_scope_changed");
1021
+ return resolved.scope;
1022
+ }
1023
+
1024
+ function taskArtifactOwnerTranscriptMatches(
1025
+ storage: FileSessionStorage,
1026
+ target: VerifiedSessionDeleteTarget,
1027
+ evidence: TaskArtifactOwnerDeletionEvidence,
1028
+ ): boolean {
1029
+ const transcriptPath = target.detachedTranscriptPath ?? target.transcriptPath;
1030
+ try {
1031
+ const parent = fsSync.lstatSync(path.dirname(transcriptPath), { bigint: true });
1032
+ if (
1033
+ !parent.isDirectory() ||
1034
+ parent.isSymbolicLink() ||
1035
+ parent.dev !== target.transcriptParentIdentity?.dev ||
1036
+ parent.ino !== target.transcriptParentIdentity?.ino
1037
+ )
1038
+ return false;
1039
+ const snapshot = storage.readSnapshotSync(transcriptPath);
1040
+ const digest = createHash("sha256").update(snapshot.bytes).digest("hex");
1041
+ if (
1042
+ !snapshot.stat.isFile ||
1043
+ snapshot.stat.dev !== target.transcriptIdentity.dev ||
1044
+ snapshot.stat.ino !== target.transcriptIdentity.ino ||
1045
+ snapshot.stat.nlink !== target.transcriptIdentity.nlink ||
1046
+ snapshot.stat.size !== target.transcriptIdentity.size ||
1047
+ snapshot.stat.mtimeNs !== target.transcriptIdentity.mtimeNs ||
1048
+ digest !== target.transcriptIdentity.sha256
1049
+ )
1050
+ return false;
1051
+ const header = parseFirstJsonlLine(snapshot.bytes);
1052
+ if (
1053
+ !header ||
1054
+ typeof header.cwd !== "string" ||
1055
+ canonicalExistingPath(header.cwd) !== canonicalExistingPath(target.cwd)
1056
+ )
1057
+ return false;
1058
+ const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, target.sessionId);
1059
+ return (
1060
+ locator?.schemaVersion === evidence.locator.schemaVersion &&
1061
+ locator.ownerId === evidence.locator.ownerId &&
1062
+ locator.directoryDev === evidence.locator.directoryDev &&
1063
+ locator.directoryIno === evidence.locator.directoryIno
1064
+ );
1065
+ } catch {
1066
+ return false;
1067
+ }
1068
+ }
1069
+
1070
+ function taskArtifactOwnerFailureDiagnostic(error: unknown): string {
1071
+ const message = error instanceof Error ? error.message : String(error);
1072
+ return (
1073
+ message.startsWith("task_artifact_owner_") ? message : `task_artifact_owner_cleanup_failed:${message}`
1074
+ ).slice(0, 4096);
1075
+ }
1076
+
1077
+ function taskArtifactOwnerRetirementDisposition(
1078
+ outcome: TaskArtifactOwnerRetirementOutcome,
1079
+ ): BrokerTaskArtifactOwnerRetirementDisposition {
1080
+ return serializeBrokerTaskArtifactOwnerRetirementDisposition(outcome);
1081
+ }
1082
+
1083
+ function captureTaskArtifactOwnerEvidence(
1084
+ storage: FileSessionStorage,
1085
+ target: VerifiedSessionDeleteTarget,
1086
+ ): { evidence?: TaskArtifactOwnerDeletionEvidence; error?: string } {
1087
+ const transcriptPath = target.detachedTranscriptPath ?? target.transcriptPath;
1088
+ let snapshot: SessionStorageSnapshot;
1089
+ try {
1090
+ snapshot = storage.readSnapshotSync(transcriptPath);
1091
+ } catch {
1092
+ return { error: "task_artifact_owner_transcript_unavailable" };
1093
+ }
1094
+ const digest = createHash("sha256").update(snapshot.bytes).digest("hex");
1095
+ if (
1096
+ !snapshot.stat.isFile ||
1097
+ snapshot.stat.dev !== target.transcriptIdentity.dev ||
1098
+ snapshot.stat.ino !== target.transcriptIdentity.ino ||
1099
+ snapshot.stat.nlink !== target.transcriptIdentity.nlink ||
1100
+ snapshot.stat.size !== target.transcriptIdentity.size ||
1101
+ snapshot.stat.mtimeNs !== target.transcriptIdentity.mtimeNs ||
1102
+ digest !== target.transcriptIdentity.sha256
1103
+ )
1104
+ return { error: "task_artifact_owner_transcript_identity_mismatch" };
1105
+ try {
1106
+ const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, target.sessionId);
1107
+ if (!locator) return {};
1108
+ const context = target.taskArtifactOwnerStorageContext;
1109
+ if (!context) return { error: "task_artifact_owner_context_missing" };
1110
+ const evidence = captureTaskArtifactOwnerDeletionEvidence(context, target.sessionId, locator);
1111
+ return evidence ? { evidence } : { error: "task_artifact_owner_evidence_missing" };
1112
+ } catch (error) {
1113
+ return { error: taskArtifactOwnerFailureDiagnostic(error) };
1114
+ }
1115
+ }
1116
+
964
1117
  type CleanupIdentity = {
965
1118
  dev: bigint;
966
1119
  ino: bigint;
@@ -1667,9 +1820,9 @@ export async function reapDeadLifecycleMarkers(
1667
1820
  let inspected = 0;
1668
1821
  for await (const entry of directoryHandle) {
1669
1822
  if (inspected >= inspectionLimit) break;
1670
- inspected += 1;
1671
1823
  const name = entry.name;
1672
1824
  if (!entry.isFile() || !name.endsWith(".lifecycle.json")) continue;
1825
+ inspected += 1;
1673
1826
  const id = name.slice(0, -".lifecycle.json".length);
1674
1827
  if (!isCanonicalSessionId(id)) continue;
1675
1828
  const markerPath = path.join(directory, name);
@@ -1741,6 +1894,108 @@ export async function reapDeadLifecycleMarkers(
1741
1894
  return reaped;
1742
1895
  }
1743
1896
 
1897
+ /**
1898
+ * Retires one launch id's lifecycle marker pair after its recorded owner has
1899
+ * exited. Unlike the bounded background sweep, this launch-local repair is
1900
+ * not age-gated and does not require the ready sibling to contain the same
1901
+ * effect marker: the primary marker is the owner authority for this id.
1902
+ */
1903
+ type RetireExitedLifecycleMarkerPairHook = () => void | Promise<void>;
1904
+
1905
+ async function retireExitedLifecycleMarkerPairImpl(
1906
+ root: string,
1907
+ id: string,
1908
+ afterObservation?: RetireExitedLifecycleMarkerPairHook,
1909
+ ): Promise<boolean> {
1910
+ if (!isCanonicalSessionId(id)) return false;
1911
+ let directory: string;
1912
+ let directoryIdentity: { dev: bigint; ino: bigint };
1913
+ try {
1914
+ const canonicalRoot = fsSync.realpathSync(root);
1915
+ directory = path.join(canonicalRoot, "sdk");
1916
+ const directoryStat = fsSync.lstatSync(directory, { bigint: true });
1917
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) return false;
1918
+ directoryIdentity = { dev: directoryStat.dev, ino: directoryStat.ino };
1919
+ } catch {
1920
+ return false;
1921
+ }
1922
+ const markerPath = lifecycleMarkerPath(path.dirname(directory), id);
1923
+ const primary = captureLifecycleFile(markerPath, true, true);
1924
+ if (!primary) return false;
1925
+ let marker: EffectMarker;
1926
+ try {
1927
+ const parsed = parseLifecycleJson(primary.bytes);
1928
+ if (!isExactEffectMarker(parsed)) return false;
1929
+ marker = parsed;
1930
+ } catch {
1931
+ return false;
1932
+ }
1933
+ if (observeProcess(marker.pid, marker.incarnation) !== "exited") return false;
1934
+ await afterObservation?.();
1935
+ const readyPath = lifecycleReadyPath(path.dirname(directory), id);
1936
+ const ready = captureLifecycleFile(readyPath, true, true);
1937
+ try {
1938
+ const currentParent = lifecycleParentIdentity(directory);
1939
+ if (
1940
+ !currentParent ||
1941
+ BigInt(currentParent.dev) !== directoryIdentity.dev ||
1942
+ BigInt(currentParent.ino) !== directoryIdentity.ino
1943
+ )
1944
+ return false;
1945
+ const currentPrimary = captureLifecycleFile(markerPath, true, true);
1946
+ await afterObservation?.();
1947
+ if (
1948
+ !currentPrimary ||
1949
+ !sameLifecycleCleanupIdentity(
1950
+ currentPrimary.identity,
1951
+ serializeCleanupIdentity({ ...primary.identity, size: Number(primary.identity.size) }),
1952
+ )
1953
+ )
1954
+ return false;
1955
+ const currentMarker = parseLifecycleJson(currentPrimary.bytes);
1956
+ if (!isExactEffectMarker(currentMarker) || !sameEffectMarker(currentMarker, marker)) return false;
1957
+ if (ready) {
1958
+ const currentReady = captureLifecycleFile(readyPath, true, true);
1959
+ if (
1960
+ !currentReady ||
1961
+ !sameLifecycleCleanupIdentity(
1962
+ currentReady.identity,
1963
+ serializeCleanupIdentity({ ...ready.identity, size: Number(ready.identity.size) }),
1964
+ )
1965
+ )
1966
+ return false;
1967
+ const removedReady = nativeLifecycle().exactUnlinkDirect(readyPath, {
1968
+ ...ready.identity,
1969
+ parentDev: directoryIdentity.dev,
1970
+ parentIno: directoryIdentity.ino,
1971
+ quarantineName: `.gjc-reap-${randomUUID()}-${path.basename(readyPath)}`,
1972
+ });
1973
+ if (!removedReady.ok && removedReady.code !== "not_found") return false;
1974
+ }
1975
+ const removedMarker = nativeLifecycle().exactUnlinkDirect(markerPath, {
1976
+ ...primary.identity,
1977
+ parentDev: directoryIdentity.dev,
1978
+ parentIno: directoryIdentity.ino,
1979
+ quarantineName: `.gjc-reap-${randomUUID()}-${path.basename(markerPath)}`,
1980
+ });
1981
+ return removedMarker.ok || removedMarker.code === "not_found";
1982
+ } catch {
1983
+ return false;
1984
+ }
1985
+ }
1986
+
1987
+ export async function retireExitedLifecycleMarkerPair(root: string, id: string): Promise<boolean> {
1988
+ return retireExitedLifecycleMarkerPairImpl(root, id);
1989
+ }
1990
+
1991
+ export async function retireExitedLifecycleMarkerPairForTest(
1992
+ root: string,
1993
+ id: string,
1994
+ afterObservation: RetireExitedLifecycleMarkerPairHook,
1995
+ ): Promise<boolean> {
1996
+ return retireExitedLifecycleMarkerPairImpl(root, id, afterObservation);
1997
+ }
1998
+
1744
1999
  export async function writeEffectMarker(root: string, id: string, marker: EffectMarker): Promise<void> {
1745
2000
  const directory = path.join(root, "sdk");
1746
2001
  await fs.mkdir(directory, { recursive: true, mode: 0o700 });
@@ -5392,12 +5647,11 @@ async function currentReadyAuthority(
5392
5647
  token?: unknown;
5393
5648
  pid?: unknown;
5394
5649
  };
5395
- // Native-alive owned readiness is the admission authority. `record.live`
5396
- // also requires a fresh index heartbeat projection, which can lag a just-
5397
- // registered detached Windows host. Never admit a terminal/uncertain or
5398
- // native-dead child; do not refuse a native-alive ready host for a stale live bit.
5399
- await broker.heartbeatSessions();
5400
- await broker.index.refresh();
5650
+ // Native-alive owned readiness is the admission authority. The indexed
5651
+ // projection can lag a just-registered detached Windows host, so the
5652
+ // change-stamp refresh is deliberately separate from heartbeat liveness.
5653
+ // Never admit a terminal/uncertain or native-dead child.
5654
+ await broker.index.refreshIfChanged();
5401
5655
  const record = broker.index
5402
5656
  .listSessions()
5403
5657
  .sessions.find(
@@ -5864,6 +6118,8 @@ type ValidatedDelete = {
5864
6118
  target: VerifiedSessionDeleteTarget;
5865
6119
  metadataRoot: string;
5866
6120
  transcriptParentIdentity: { dev: string; ino: string };
6121
+ taskArtifactOwnerCaptureError?: string;
6122
+ inspectProtocol?: ManagedGcProtocolScopeInspector;
5867
6123
  };
5868
6124
  function cleanupIdentity(
5869
6125
  identity: BrokerCleanupEvidence["transcriptIdentity"],
@@ -5937,8 +6193,12 @@ function replayDeleteTarget(cleanup: CleanupEvidence): ValidatedDelete | BrokerR
5937
6193
  (artifactsIdentity.dev !== artifactTreeIdentity.dev || artifactsIdentity.ino !== artifactTreeIdentity.ino)
5938
6194
  )
5939
6195
  return fail("terminal_uncertain", "Artifact cleanup tree does not match its ledger-bound root identity.");
5940
- if (cleanup.phase === "artifacts" && cleanup.artifactsRemoved === true)
5941
- return fail("terminal_uncertain", "Artifacts-phase cleanup receipt falsely claims artifact completion.");
6196
+ if (
6197
+ cleanup.phase === "artifacts" &&
6198
+ cleanup.artifactsRemoved === true &&
6199
+ cleanup.taskArtifactOwnerDeletionEvidence === undefined
6200
+ )
6201
+ return fail("terminal_uncertain", "Artifacts-phase cleanup receipt lacks immutable owner evidence.");
5942
6202
  if (
5943
6203
  cleanup.artifactsRemoved === true &&
5944
6204
  cleanup.artifactTree &&
@@ -6142,7 +6402,126 @@ async function validateDeletePath(
6142
6402
  canonicalExistingPath(replay.metadataRoot) !== canonicalRequestedRoot
6143
6403
  )
6144
6404
  return fail("invalid_input", "Cleanup receipt does not match the requested saved-session locator.");
6145
- return replay;
6405
+ const ownerFields = brokerTaskArtifactOwnerCleanupFields(cleanup);
6406
+ if (Object.keys(ownerFields).length === 0) {
6407
+ const sessionsRoot = canonicalExistingPath(getSessionsDir(broker.settings.agentDir));
6408
+ const relative = path.relative(sessionsRoot, replay.target.transcriptPath);
6409
+ if (
6410
+ path.resolve(replay.target.sessionsRoot) !== sessionsRoot ||
6411
+ relative === "" ||
6412
+ relative === ".." ||
6413
+ relative.startsWith(`..${path.sep}`) ||
6414
+ path.isAbsolute(relative)
6415
+ )
6416
+ return fail("terminal_uncertain", "Cleanup receipt does not match the configured session root.");
6417
+ return replay;
6418
+ }
6419
+ const inventory = await managedCandidates(broker, cwd, "Saved");
6420
+ if ("ok" in inventory) return inventory;
6421
+ const transcriptRelative = path.relative(inventory.scope.sessionsRoot, replay.target.transcriptPath);
6422
+ if (
6423
+ path.resolve(replay.target.sessionsRoot) !== inventory.scope.sessionsRoot ||
6424
+ transcriptRelative === "" ||
6425
+ transcriptRelative === ".." ||
6426
+ transcriptRelative.startsWith(`..${path.sep}`) ||
6427
+ path.isAbsolute(transcriptRelative)
6428
+ )
6429
+ return fail("terminal_uncertain", "Cleanup receipt does not match the current managed session authority.");
6430
+ replay.target.sessionsRoot = inventory.scope.sessionsRoot;
6431
+ let ownerContext: TaskArtifactOwnerStorageContext;
6432
+ let ownerScope: ManagedScope;
6433
+ try {
6434
+ ownerScope = managedOwnerScopeFromInventory(inventory.scope);
6435
+ ownerContext = taskArtifactOwnerStorageContextForScope(ownerScope);
6436
+ } catch {
6437
+ return fail(
6438
+ "terminal_uncertain",
6439
+ "Managed task-artifact-owner authority could not be restored for cleanup replay.",
6440
+ );
6441
+ }
6442
+ let owner: BrokerTaskArtifactOwnerCleanupValidation;
6443
+ try {
6444
+ owner = decodeBrokerTaskArtifactOwnerCleanupFields(
6445
+ ownerFields,
6446
+ ownerContext,
6447
+ id,
6448
+ cleanup.phase,
6449
+ cleanup.artifactsRemoved,
6450
+ );
6451
+ } catch {
6452
+ return fail("terminal_uncertain", "Cleanup receipt contains invalid task-artifact owner state.");
6453
+ }
6454
+ let captureError = owner.cleanupDiagnostic;
6455
+ if (!owner.deletionEvidence && captureError === "task_artifact_owner_writer_not_quiescent") {
6456
+ const retainedPaths = [
6457
+ replay.target.detachedArtifactsPath,
6458
+ replay.target.detachedTranscriptPath,
6459
+ replay.target.retainedArtifactsSuccessorPath,
6460
+ replay.target.retainedArtifactsPlaceholderPath,
6461
+ replay.target.retainedArtifactsUnknownPath,
6462
+ replay.target.retainedTranscriptSuccessorPath,
6463
+ replay.target.retainedTranscriptPlaceholderPath,
6464
+ replay.target.retainedTranscriptUnknownPath,
6465
+ ];
6466
+ const plannedPaths = [
6467
+ cleanup.plannedArtifactsPath,
6468
+ cleanup.artifactTree?.plannedPath,
6469
+ cleanup.plannedTranscriptPath,
6470
+ ];
6471
+ const occupiedPlan = plannedPaths.some(planned => {
6472
+ if (!planned) return false;
6473
+ return [planned, `${planned}.removing`].some(candidate => {
6474
+ try {
6475
+ fsSync.lstatSync(candidate);
6476
+ return true;
6477
+ } catch (error) {
6478
+ return (error as NodeJS.ErrnoException).code !== "ENOENT";
6479
+ }
6480
+ });
6481
+ });
6482
+ if (
6483
+ cleanup.phase !== "artifacts" ||
6484
+ cleanup.artifactsRemoved === true ||
6485
+ Object.keys(ownerFields).some(key => key !== "taskArtifactOwnerCleanupError") ||
6486
+ retainedPaths.some(candidate => candidate !== undefined) ||
6487
+ occupiedPlan ||
6488
+ transcriptParentStat.dev.toString() !== replay.transcriptParentIdentity.dev ||
6489
+ transcriptParentStat.ino.toString() !== replay.transcriptParentIdentity.ino
6490
+ )
6491
+ return fail(
6492
+ "terminal_uncertain",
6493
+ "Initial task-artifact owner capture cannot be retried after unverified cleanup effects.",
6494
+ );
6495
+ replay.target.taskArtifactOwnerStorageContext = ownerContext;
6496
+ const captured = captureTaskArtifactOwnerEvidence(replay.storage, replay.target);
6497
+ if (!captured.evidence && !captured.error)
6498
+ return fail(
6499
+ "terminal_uncertain",
6500
+ "The original task-artifact owner claim could not be restored for capture retry.",
6501
+ );
6502
+ if (captured.evidence) {
6503
+ replay.target.taskArtifactOwnerDeletionEvidence = captured.evidence;
6504
+ replay.target.deferTaskArtifactOwnerRetirement = true;
6505
+ }
6506
+ captureError = captured.error;
6507
+ }
6508
+ if (owner.deletionEvidence) {
6509
+ replay.target.taskArtifactOwnerStorageContext = ownerContext;
6510
+ replay.target.taskArtifactOwnerDeletionEvidence = owner.deletionEvidence;
6511
+ replay.target.deferTaskArtifactOwnerRetirement = true;
6512
+ if (owner.retirementContinuation)
6513
+ replay.target.taskArtifactOwnerRetirementContinuation = owner.retirementContinuation;
6514
+ if (owner.retirementOutcome) replay.target.taskArtifactOwnerRetirementOutcome = owner.retirementOutcome;
6515
+ if (owner.retired) replay.target.taskArtifactOwnerRetired = true;
6516
+ if (owner.payloadRetired) replay.target.taskArtifactOwnerPayloadRetired = true;
6517
+ if (owner.namespaceRetained) replay.target.taskArtifactOwnerNamespaceRetained = true;
6518
+ if (owner.transcriptDeleted) replay.target.taskArtifactOwnerTranscriptDeleted = true;
6519
+ }
6520
+ return {
6521
+ ...replay,
6522
+ inspectProtocol: managedGcProtocolScopeInspectorForScope(ownerScope),
6523
+ ...(captureError && !owner.deletionEvidence ? { taskArtifactOwnerCaptureError: captureError } : {}),
6524
+ };
6146
6525
  }
6147
6526
  const inventory = await managedCandidates(broker, cwd, "Saved");
6148
6527
  if ("ok" in inventory) return inventory;
@@ -6154,7 +6533,6 @@ async function validateDeletePath(
6154
6533
  const match = matches[0]!;
6155
6534
  if (inventory.migrationPolicy === "disabled" && match.provenance === "legacy")
6156
6535
  return fail("legacy_migration_disabled", "Saved legacy session migration is disabled for this workspace.");
6157
-
6158
6536
  const storage = new FileSessionStorage();
6159
6537
  let snapshot: SessionStorageSnapshot;
6160
6538
  try {
@@ -6183,28 +6561,56 @@ async function validateDeletePath(
6183
6561
  } catch {
6184
6562
  return fail("invalid_input", "session.delete transcript parent changed during authorization.");
6185
6563
  }
6564
+ let ownerContext: TaskArtifactOwnerStorageContext | undefined;
6565
+ let ownerScope: ManagedScope | undefined;
6566
+ let ownerCaptureError: string | undefined;
6567
+ let ownerLocator: TaskArtifactOwnerLocator | undefined;
6568
+ try {
6569
+ ownerLocator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, id);
6570
+ } catch (error) {
6571
+ ownerCaptureError = taskArtifactOwnerFailureDiagnostic(error);
6572
+ }
6573
+ if (ownerLocator) {
6574
+ try {
6575
+ ownerScope = managedOwnerScopeFromInventory(inventory.scope);
6576
+ ownerContext = taskArtifactOwnerStorageContextForScope(ownerScope);
6577
+ } catch {
6578
+ return fail("invalid_input", "Managed task-artifact-owner authority could not be established for deletion.");
6579
+ }
6580
+ }
6581
+ const target: VerifiedSessionDeleteTarget = {
6582
+ sessionsRoot: canonicalExistingPath(inventory.scope.sessionsRoot),
6583
+ transcriptPath: candidatePath,
6584
+ sessionId: id,
6585
+ cwd,
6586
+ transcriptIdentity: {
6587
+ dev: snapshot.stat.dev,
6588
+ ino: snapshot.stat.ino,
6589
+ nlink: snapshot.stat.nlink,
6590
+ size: snapshot.stat.size,
6591
+ mtimeNs: snapshot.stat.mtimeNs,
6592
+ sha256: digest,
6593
+ },
6594
+ transcriptParentIdentity: { dev: transcriptParentStat.dev, ino: transcriptParentStat.ino },
6595
+ ...(ownerContext ? { taskArtifactOwnerStorageContext: ownerContext } : {}),
6596
+ };
6597
+ const capturedOwner: { evidence?: TaskArtifactOwnerDeletionEvidence; error?: string } = ownerCaptureError
6598
+ ? { error: ownerCaptureError }
6599
+ : captureTaskArtifactOwnerEvidence(storage, target);
6600
+ if (capturedOwner.evidence) {
6601
+ target.taskArtifactOwnerDeletionEvidence = capturedOwner.evidence;
6602
+ target.deferTaskArtifactOwnerRetirement = true;
6603
+ }
6186
6604
  return {
6187
6605
  storage,
6188
- target: {
6189
- sessionsRoot: canonicalExistingPath(inventory.scope.sessionsRoot),
6190
- transcriptPath: candidatePath,
6191
- sessionId: id,
6192
- cwd,
6193
- transcriptIdentity: {
6194
- dev: snapshot.stat.dev,
6195
- ino: snapshot.stat.ino,
6196
- nlink: snapshot.stat.nlink,
6197
- size: snapshot.stat.size,
6198
- mtimeNs: snapshot.stat.mtimeNs,
6199
- sha256: digest,
6200
- },
6201
- transcriptParentIdentity: { dev: transcriptParentStat.dev, ino: transcriptParentStat.ino },
6202
- },
6606
+ target,
6203
6607
  metadataRoot: canonicalRequestedRoot,
6608
+ ...(ownerScope ? { inspectProtocol: managedGcProtocolScopeInspectorForScope(ownerScope) } : {}),
6204
6609
  transcriptParentIdentity: {
6205
6610
  dev: transcriptParentStat.dev.toString(),
6206
6611
  ino: transcriptParentStat.ino.toString(),
6207
6612
  },
6613
+ ...(capturedOwner.error ? { taskArtifactOwnerCaptureError: capturedOwner.error } : {}),
6208
6614
  };
6209
6615
  }
6210
6616
  type CloseAuthority = { endpointGeneration: number; endpointIncarnation: string };
@@ -6347,7 +6753,13 @@ async function executeLifecycleResponse(
6347
6753
  if (requestedSourceSessionId !== undefined && !isCanonicalSessionId(requestedSourceSessionId))
6348
6754
  return fail("invalid_input", "sourceSessionId must be a canonical safe identifier.");
6349
6755
  if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
6350
- await broker.index.refresh();
6756
+ // Creates without managed-worktree admission have no existing session
6757
+ // authority to reconcile, so the change-stamp fast path avoids replaying
6758
+ // the full index under its lock on every ACP spawn. Managed-worktree creates
6759
+ // must serialize with registrations before checking checkout occupancy.
6760
+ if (operation === "session.create" && lifecycleWorktreeTarget(input) === undefined)
6761
+ await broker.index.refreshIfChanged();
6762
+ else await broker.index.refresh();
6351
6763
  if (operation === "session.create") {
6352
6764
  // Creation has no existing session authority to reconcile. Keep the
6353
6765
  // broker's periodic/startup heartbeat checkpoint as the reaping owner,
@@ -6650,6 +7062,7 @@ async function executeLifecycleResponse(
6650
7062
  terminationStartDeadline = childDeadlines.terminationStartDeadlineAt;
6651
7063
  lifecycleDeadline = childDeadlines.lifecycleCleanupDeadlineAt;
6652
7064
  }
7065
+ await retireExitedLifecycleMarkerPair(launch.root, launch.id);
6653
7066
  await reapDeadLifecycleMarkers(launch.root);
6654
7067
  } catch (error) {
6655
7068
  if (launch.worktreePlan && worktreeReceipt?.created && !worktreeReceipt.reused) {
@@ -7381,15 +7794,45 @@ async function executeLifecycleResponse(
7381
7794
  const transcriptParentIdentity = cleanup?.transcriptParentIdentity ?? validated.transcriptParentIdentity;
7382
7795
  const durableArtifactsPlan =
7383
7796
  cleanup?.artifactTree?.plannedPath ?? cleanup?.plannedArtifactsPath ?? cleanupTarget.plannedArtifactsPath;
7797
+ let ownerRetirementOutcome = cleanupTarget.taskArtifactOwnerRetirementOutcome
7798
+ ? taskArtifactOwnerRetirementDisposition(cleanupTarget.taskArtifactOwnerRetirementOutcome)
7799
+ : undefined;
7800
+ let ownerTranscriptDeleted = cleanupTarget.taskArtifactOwnerTranscriptDeleted;
7801
+ let freshPayloadRetirementForThisAttempt = false;
7802
+ const ownerNamespacePending =
7803
+ cleanupTarget.taskArtifactOwnerPayloadRetired === true &&
7804
+ cleanupTarget.taskArtifactOwnerNamespaceRetained === true &&
7805
+ cleanupTarget.taskArtifactOwnerRetired !== true;
7384
7806
  const preauthorizedCleanup: CleanupEvidence = {
7385
7807
  cleanupReceiptVersion: 1,
7386
- phase: cleanupTarget.artifactsRemoved ? "transcript" : "artifacts",
7808
+ phase:
7809
+ cleanupTarget.artifactsRemoved &&
7810
+ !ownerNamespacePending &&
7811
+ (cleanupTarget.taskArtifactOwnerRetired === true || !cleanupTarget.taskArtifactOwnerDeletionEvidence)
7812
+ ? "transcript"
7813
+ : "artifacts",
7387
7814
  sessionId: cleanupTarget.sessionId,
7388
7815
  sessionsRoot: cleanupTarget.sessionsRoot,
7389
7816
  transcriptPath: cleanupTarget.transcriptPath,
7390
7817
  cwd: cleanupTarget.cwd,
7391
7818
  ...(cleanupTarget.artifactsRemoved ? { artifactsRemoved: true } : {}),
7392
7819
  ...(cleanupTarget.artifactsAbsentAtAuthorization ? { artifactsAbsentAtAuthorization: true as const } : {}),
7820
+ ...(cleanupTarget.taskArtifactOwnerDeletionEvidence
7821
+ ? { taskArtifactOwnerDeletionEvidence: cleanupTarget.taskArtifactOwnerDeletionEvidence }
7822
+ : {}),
7823
+ ...(cleanupTarget.taskArtifactOwnerRetirementContinuation
7824
+ ? { taskArtifactOwnerRetirementContinuation: cleanupTarget.taskArtifactOwnerRetirementContinuation }
7825
+ : {}),
7826
+ ...(ownerRetirementOutcome ? { taskArtifactOwnerRetirementOutcome: ownerRetirementOutcome } : {}),
7827
+ ...(cleanupTarget.taskArtifactOwnerPayloadRetired ? { taskArtifactOwnerPayloadRetired: true as const } : {}),
7828
+ ...(cleanupTarget.taskArtifactOwnerNamespaceRetained
7829
+ ? { taskArtifactOwnerNamespaceRetained: true as const }
7830
+ : {}),
7831
+ ...(cleanupTarget.taskArtifactOwnerRetired ? { taskArtifactOwnerRetired: true as const } : {}),
7832
+ ...(ownerTranscriptDeleted ? { taskArtifactOwnerTranscriptDeleted: true as const } : {}),
7833
+ ...(validated.taskArtifactOwnerCaptureError
7834
+ ? { taskArtifactOwnerCleanupError: validated.taskArtifactOwnerCaptureError }
7835
+ : {}),
7393
7836
  metadataRoot: validated.metadataRoot,
7394
7837
  transcriptIdentity: serializeCleanupIdentity(cleanupTarget.transcriptIdentity),
7395
7838
  transcriptParentIdentity,
@@ -7489,6 +7932,145 @@ async function executeLifecycleResponse(
7489
7932
  });
7490
7933
  return pending;
7491
7934
  };
7935
+ const cleanupWithFreshOwnerPayload = (receipt: CleanupEvidence): CleanupEvidence => {
7936
+ const current = { ...receipt };
7937
+ delete current.taskArtifactOwnerDeletionEvidence;
7938
+ delete current.taskArtifactOwnerRetirementContinuation;
7939
+ delete current.taskArtifactOwnerRetirementOutcome;
7940
+ delete current.taskArtifactOwnerPayloadRetired;
7941
+ delete current.taskArtifactOwnerNamespaceRetained;
7942
+ delete current.taskArtifactOwnerRetired;
7943
+ delete current.taskArtifactOwnerTranscriptDeleted;
7944
+ delete current.taskArtifactOwnerCleanupError;
7945
+ return {
7946
+ ...current,
7947
+ ...(cleanupTarget.taskArtifactOwnerDeletionEvidence
7948
+ ? { taskArtifactOwnerDeletionEvidence: cleanupTarget.taskArtifactOwnerDeletionEvidence }
7949
+ : {}),
7950
+ ...(cleanupTarget.taskArtifactOwnerRetirementContinuation
7951
+ ? { taskArtifactOwnerRetirementContinuation: cleanupTarget.taskArtifactOwnerRetirementContinuation }
7952
+ : {}),
7953
+ ...(ownerRetirementOutcome ? { taskArtifactOwnerRetirementOutcome: ownerRetirementOutcome } : {}),
7954
+ ...(cleanupTarget.taskArtifactOwnerPayloadRetired
7955
+ ? { taskArtifactOwnerPayloadRetired: true as const }
7956
+ : {}),
7957
+ ...(cleanupTarget.taskArtifactOwnerNamespaceRetained
7958
+ ? { taskArtifactOwnerNamespaceRetained: true as const }
7959
+ : {}),
7960
+ ...(cleanupTarget.taskArtifactOwnerRetired ? { taskArtifactOwnerRetired: true as const } : {}),
7961
+ ...(ownerTranscriptDeleted ? { taskArtifactOwnerTranscriptDeleted: true as const } : {}),
7962
+ };
7963
+ };
7964
+ const publishTaskArtifactOwnerPending = async (
7965
+ error: unknown,
7966
+ receipt: CleanupEvidence = preauthorizedCleanup,
7967
+ ): Promise<BrokerResponse> => {
7968
+ const diagnostic = taskArtifactOwnerFailureDiagnostic(error);
7969
+ const evidence = cleanupTarget.taskArtifactOwnerDeletionEvidence;
7970
+ const ownerReceipt: CleanupEvidence = {
7971
+ ...cleanupWithFreshOwnerPayload(receipt),
7972
+ phase: "artifacts",
7973
+ artifactsRemoved: receipt.artifactsRemoved === true && evidence ? true : undefined,
7974
+ artifactsAbsentAtAuthorization:
7975
+ receipt.artifactsRemoved === true ? undefined : receipt.artifactsAbsentAtAuthorization,
7976
+ taskArtifactOwnerCleanupError: diagnostic,
7977
+ };
7978
+ const pending = fail(
7979
+ "cleanup_pending",
7980
+ `Saved session cleanup is pending in artifacts: ${diagnostic}`,
7981
+ ownerReceipt,
7982
+ );
7983
+ await broker.ledger.transition(identity, "effect_started", {
7984
+ intendedSessionId: id,
7985
+ response: pending,
7986
+ });
7987
+ return pending;
7988
+ };
7989
+ const persistFreshTaskArtifactOwnerRetirement = async (
7990
+ receipt: CleanupEvidence,
7991
+ ): Promise<BrokerResponse | undefined> => {
7992
+ const evidence = cleanupTarget.taskArtifactOwnerDeletionEvidence;
7993
+ if (!evidence) return undefined;
7994
+ const ownerContext = cleanupTarget.taskArtifactOwnerStorageContext;
7995
+ if (!ownerContext)
7996
+ return await publishTaskArtifactOwnerPending("task_artifact_owner_context_missing", receipt);
7997
+ if (
7998
+ ownerTranscriptDeleted === true
7999
+ ? !logicalTranscriptRetirementMatches()
8000
+ : !taskArtifactOwnerTranscriptMatches(validated.storage, cleanupTarget, evidence)
8001
+ )
8002
+ return await publishTaskArtifactOwnerPending("task_artifact_owner_transcript_identity_mismatch", receipt);
8003
+ let outcome: TaskArtifactOwnerRetirementOutcome;
8004
+ try {
8005
+ if (
8006
+ await hasSiblingTaskArtifactOwnerTranscript(
8007
+ validated.storage,
8008
+ cleanupTarget.transcriptPath,
8009
+ evidence.locator,
8010
+ ownerContext,
8011
+ validated.inspectProtocol,
8012
+ )
8013
+ )
8014
+ return await publishTaskArtifactOwnerPending(
8015
+ "task_artifact_owner_shared_with_sibling_transcript",
8016
+ receipt,
8017
+ );
8018
+ outcome = retireTaskArtifactOwner(
8019
+ ownerContext,
8020
+ evidence,
8021
+ cleanupTarget.taskArtifactOwnerRetirementContinuation,
8022
+ );
8023
+ } catch (error) {
8024
+ freshPayloadRetirementForThisAttempt = false;
8025
+ return await publishTaskArtifactOwnerPending(error, receipt);
8026
+ }
8027
+ freshPayloadRetirementForThisAttempt = outcome.kind === "payload_retired";
8028
+ cleanupTarget.taskArtifactOwnerRetirementOutcome = outcome;
8029
+ ownerRetirementOutcome = taskArtifactOwnerRetirementDisposition(outcome);
8030
+ cleanupTarget.taskArtifactOwnerRetired = outcome.kind === "completed" ? true : undefined;
8031
+ cleanupTarget.taskArtifactOwnerRetirementContinuation =
8032
+ outcome.kind === "completed" ? undefined : outcome.continuation;
8033
+ cleanupTarget.taskArtifactOwnerPayloadRetired = outcome.kind === "payload_retired" ? true : undefined;
8034
+ cleanupTarget.taskArtifactOwnerNamespaceRetained = outcome.kind === "payload_retired" ? true : undefined;
8035
+ if (outcome.kind === "completed") ownerTranscriptDeleted = undefined;
8036
+ const ownerError =
8037
+ outcome.kind === "uncertain"
8038
+ ? taskArtifactOwnerFailureDiagnostic(outcome.reason)
8039
+ : outcome.kind === "cleanup_pending"
8040
+ ? taskArtifactOwnerFailureDiagnostic(
8041
+ outcome.nativeOutcome?.code ?? "task_artifact_owner_cleanup_pending",
8042
+ )
8043
+ : undefined;
8044
+ const ownerReceipt: CleanupEvidence = {
8045
+ ...cleanupWithFreshOwnerPayload(receipt),
8046
+ phase: "artifacts",
8047
+ artifactsRemoved: true,
8048
+ artifactsAbsentAtAuthorization: undefined,
8049
+ ...(ownerError ? { taskArtifactOwnerCleanupError: ownerError } : {}),
8050
+ };
8051
+ const stateResponse = fail(
8052
+ "cleanup_pending",
8053
+ outcome.kind === "payload_retired"
8054
+ ? "Task-artifact payload is retired; its native namespace remains pending in artifacts."
8055
+ : outcome.kind === "completed"
8056
+ ? "Task-artifact owner retirement is durably complete; transcript cleanup is preauthorized."
8057
+ : `Task-artifact owner retirement remains ${outcome.kind} in artifacts.`,
8058
+ ownerReceipt,
8059
+ );
8060
+ await broker.ledger.transition(identity, "effect_started", { intendedSessionId: id, response: stateResponse });
8061
+ return outcome.kind === "completed" || outcome.kind === "payload_retired" ? undefined : stateResponse;
8062
+ };
8063
+ const storageOwnerFields = (): Partial<VerifiedSessionDeleteTarget> => {
8064
+ const evidence = cleanupTarget.taskArtifactOwnerDeletionEvidence;
8065
+ if (!evidence) return {};
8066
+ if (!cleanupTarget.taskArtifactOwnerStorageContext) throw new Error("task_artifact_owner_context_missing");
8067
+ return {
8068
+ deferTaskArtifactOwnerRetirement: true,
8069
+ ...(cleanupTarget.taskArtifactOwnerRetirementOutcome
8070
+ ? { taskArtifactOwnerRetirementOutcome: cleanupTarget.taskArtifactOwnerRetirementOutcome }
8071
+ : {}),
8072
+ };
8073
+ };
7492
8074
  const canonicalArtifactsPath = cleanupTarget.transcriptPath.slice(0, -6);
7493
8075
  const transcriptCleanupAuthorityIsAbsent = (): boolean =>
7494
8076
  [
@@ -7544,6 +8126,11 @@ async function executeLifecycleResponse(
7544
8126
  return false;
7545
8127
  }
7546
8128
  };
8129
+ const logicalTranscriptRetirementMatches = (): boolean =>
8130
+ transcriptParentMatchesPersistedIdentity() &&
8131
+ pathIsAbsent(cleanupTarget.transcriptPath) &&
8132
+ pathIsAbsent(cleanupTarget.retainedTranscriptSuccessorPath) &&
8133
+ pathIsAbsent(cleanupTarget.retainedTranscriptUnknownPath);
7547
8134
  const retainedTranscriptIdentityIsAbsentFromParent = (): boolean => {
7548
8135
  const transcriptParent = path.dirname(cleanupTarget.transcriptPath);
7549
8136
  const expectedParent = preauthorizedCleanup.transcriptParentIdentity;
@@ -7639,31 +8226,81 @@ async function executeLifecycleResponse(
7639
8226
  preauthorizedCleanup,
7640
8227
  ),
7641
8228
  });
7642
- let deleted: VerifiedSessionDeleteResult;
7643
- try {
7644
- const completedArtifactReplay = cleanup?.phase === "transcript" && cleanup.artifactsRemoved === true;
7645
- if (completedArtifactReplay && !pathIsAbsent(canonicalArtifactsPath))
8229
+ if (validated.taskArtifactOwnerCaptureError)
8230
+ return await publishTaskArtifactOwnerPending(validated.taskArtifactOwnerCaptureError);
8231
+ if (
8232
+ cleanup?.phase === "artifacts" &&
8233
+ cleanupTarget.artifactsRemoved === true &&
8234
+ cleanupTarget.taskArtifactOwnerDeletionEvidence &&
8235
+ cleanupTarget.taskArtifactOwnerRetired !== true
8236
+ ) {
8237
+ if (!pathIsAbsent(canonicalArtifactsPath))
7646
8238
  return await publishCanonicalArtifactReappearance(preauthorizedCleanup);
7647
- if (
7648
- completedArtifactReplay &&
7649
- transcriptCleanupAuthorityIsAbsent() &&
7650
- retainedTranscriptIdentityIsAbsentFromParent()
7651
- )
7652
- return fail(
7653
- "cleanup_pending",
7654
- "Saved session cleanup remains pending because transcript authority disappeared without native deletion proof.",
8239
+ const ownerPending = await persistFreshTaskArtifactOwnerRetirement(preauthorizedCleanup);
8240
+ if (ownerPending) return ownerPending;
8241
+ }
8242
+ let deleted: VerifiedSessionDeleteResult | undefined;
8243
+ if (cleanup?.taskArtifactOwnerTranscriptDeleted === true) {
8244
+ if (!logicalTranscriptRetirementMatches())
8245
+ return await publishTaskArtifactOwnerPending(
8246
+ "task_artifact_owner_transcript_disposition_mismatch",
7655
8247
  preauthorizedCleanup,
7656
8248
  );
7657
- else {
8249
+ if (cleanupTarget.taskArtifactOwnerPayloadRetired === true) {
8250
+ const residual = fail(
8251
+ "cleanup_pending",
8252
+ "Transcript retirement is complete; the task-artifact owner namespace remains pending in artifacts.",
8253
+ cleanupWithFreshOwnerPayload({ ...preauthorizedCleanup, phase: "artifacts", artifactsRemoved: true }),
8254
+ );
8255
+ await broker.ledger.transition(identity, "effect_started", { intendedSessionId: id, response: residual });
8256
+ return residual;
8257
+ }
8258
+ if (cleanupTarget.taskArtifactOwnerRetired === true) {
8259
+ const ownerEvidence = cleanupTarget.taskArtifactOwnerDeletionEvidence;
8260
+ const ownerContext = cleanupTarget.taskArtifactOwnerStorageContext;
8261
+ const ownerOutcome = cleanupTarget.taskArtifactOwnerRetirementOutcome;
8262
+ if (!ownerEvidence || !ownerContext || !ownerOutcome)
8263
+ return await publishTaskArtifactOwnerPending(
8264
+ "task_artifact_owner_retired_evidence_missing",
8265
+ preauthorizedCleanup,
8266
+ );
8267
+ try {
8268
+ verifyTaskArtifactOwnerPhysicalRetirement(ownerContext, ownerEvidence, ownerOutcome);
8269
+ } catch (error) {
8270
+ return await publishTaskArtifactOwnerPending(error, preauthorizedCleanup);
8271
+ }
8272
+ deleted = { kind: "deleted" };
8273
+ }
8274
+ }
8275
+ try {
8276
+ if (deleted === undefined) {
8277
+ const completedArtifactReplay = cleanup?.phase === "transcript" && cleanup.artifactsRemoved === true;
8278
+ if (completedArtifactReplay && !pathIsAbsent(canonicalArtifactsPath))
8279
+ return await publishCanonicalArtifactReappearance(preauthorizedCleanup);
8280
+ if (
8281
+ completedArtifactReplay &&
8282
+ transcriptCleanupAuthorityIsAbsent() &&
8283
+ retainedTranscriptIdentityIsAbsentFromParent()
8284
+ )
8285
+ return fail(
8286
+ "cleanup_pending",
8287
+ "Saved session cleanup remains pending because transcript authority disappeared without native deletion proof.",
8288
+ preauthorizedCleanup,
8289
+ );
7658
8290
  if (!transcriptParentMatchesPersistedIdentity())
7659
8291
  return fail(
7660
8292
  "cleanup_pending",
7661
8293
  "Saved session cleanup is pending because transcript parent identity changed before exact mutation.",
7662
8294
  preauthorizedCleanup,
7663
8295
  );
7664
- deleted = await validated.storage.deleteSessionVerified(cleanupTarget);
8296
+ deleted = await validated.storage.deleteSessionVerified(
8297
+ { ...cleanupTarget, ...storageOwnerFields() },
8298
+ validated.inspectProtocol,
8299
+ );
7665
8300
  }
7666
8301
  } catch (error) {
8302
+ if (error instanceof Error && error.message.startsWith("task_artifact_owner_"))
8303
+ return await publishTaskArtifactOwnerPending(error);
7667
8304
  if (error instanceof SessionDeleteVerificationError) {
7668
8305
  if (cleanup?.phase === "artifacts")
7669
8306
  return await publishChangedArtifactRoot(
@@ -7682,6 +8319,7 @@ async function executeLifecycleResponse(
7682
8319
  `Unable to delete saved session artifacts: ${error instanceof Error ? error.message : String(error)}`,
7683
8320
  );
7684
8321
  }
8322
+ if (!deleted) return fail("unavailable", "Saved session deletion did not produce an outcome.");
7685
8323
  if (
7686
8324
  deleted.kind === "deleted" &&
7687
8325
  cleanup?.phase === "transcript" &&
@@ -7705,10 +8343,22 @@ async function executeLifecycleResponse(
7705
8343
  cleanupTarget.detachedArtifactsPath,
7706
8344
  "Saved session cleanup is pending in artifacts: an authorized quarantine alias remains after exact removal.",
7707
8345
  );
7708
- const transcriptPhaseCleanup: CleanupEvidence = {
7709
- ...preauthorizedCleanup,
7710
- phase: "transcript",
8346
+ const returnedOwnerEvidence = deleted.taskArtifactOwnerDeletionEvidence;
8347
+ if (
8348
+ cleanupTarget.taskArtifactOwnerDeletionEvidence &&
8349
+ returnedOwnerEvidence &&
8350
+ JSON.stringify(cleanupTarget.taskArtifactOwnerDeletionEvidence) !== JSON.stringify(returnedOwnerEvidence)
8351
+ )
8352
+ return await publishTaskArtifactOwnerPending("task_artifact_owner_evidence_changed_during_delete");
8353
+ if (returnedOwnerEvidence && !cleanupTarget.taskArtifactOwnerDeletionEvidence)
8354
+ return await publishTaskArtifactOwnerPending("task_artifact_owner_evidence_not_prepared_before_effect");
8355
+ const ownerEvidence = cleanupTarget.taskArtifactOwnerDeletionEvidence;
8356
+ const artifactCompletionCleanup: CleanupEvidence = {
8357
+ ...cleanupWithFreshOwnerPayload(preauthorizedCleanup),
8358
+ phase: ownerEvidence ? "artifacts" : "transcript",
7711
8359
  artifactsRemoved: true,
8360
+ artifactsAbsentAtAuthorization: undefined,
8361
+ ...(ownerEvidence ? { taskArtifactOwnerDeletionEvidence: ownerEvidence } : {}),
7712
8362
  detachedArtifactsPath: undefined,
7713
8363
  retainedArtifactsSuccessorPath: undefined,
7714
8364
  retainedArtifactsPlaceholderPath: undefined,
@@ -7724,12 +8374,41 @@ async function executeLifecycleResponse(
7724
8374
  }
7725
8375
  : {}),
7726
8376
  };
7727
-
7728
8377
  await broker.ledger.transition(identity, "effect_started", {
7729
8378
  intendedSessionId: id,
7730
8379
  response: fail(
7731
8380
  "cleanup_pending",
7732
- "Saved session artifacts were removed; transcript cleanup is preauthorized.",
8381
+ "Saved session artifacts are removed; owner cleanup is durably preauthorized.",
8382
+ artifactCompletionCleanup,
8383
+ ),
8384
+ });
8385
+ if (ownerEvidence && cleanupTarget.taskArtifactOwnerRetired !== true) {
8386
+ const ownerPending = await persistFreshTaskArtifactOwnerRetirement(artifactCompletionCleanup);
8387
+ if (ownerPending) return ownerPending;
8388
+ }
8389
+ const ownerNamespaceRetained =
8390
+ cleanupTarget.taskArtifactOwnerPayloadRetired === true &&
8391
+ cleanupTarget.taskArtifactOwnerNamespaceRetained === true;
8392
+ const transcriptPhaseCleanup: CleanupEvidence = {
8393
+ ...cleanupWithFreshOwnerPayload(artifactCompletionCleanup),
8394
+ phase: ownerNamespaceRetained ? "artifacts" : "transcript",
8395
+ artifactsRemoved: true,
8396
+ artifactsAbsentAtAuthorization: undefined,
8397
+ ...(artifactCompletionCleanup.artifactTree
8398
+ ? {
8399
+ artifactTree: {
8400
+ ...artifactCompletionCleanup.artifactTree,
8401
+ detachedPath: undefined,
8402
+ completed: true as const,
8403
+ },
8404
+ }
8405
+ : {}),
8406
+ };
8407
+ await broker.ledger.transition(identity, "effect_started", {
8408
+ intendedSessionId: id,
8409
+ response: fail(
8410
+ "cleanup_pending",
8411
+ "Saved session artifacts and owner cleanup are durably recorded; transcript cleanup is preauthorized.",
7733
8412
  transcriptPhaseCleanup,
7734
8413
  ),
7735
8414
  });
@@ -7741,33 +8420,39 @@ async function executeLifecycleResponse(
7741
8420
  "Saved session cleanup remains pending because transcript authority disappeared without native deletion proof.",
7742
8421
  transcriptPhaseCleanup,
7743
8422
  );
7744
- else {
7745
- if (!transcriptParentMatchesPersistedIdentity())
7746
- return fail(
7747
- "cleanup_pending",
7748
- "Saved session cleanup is pending because transcript parent identity changed before exact mutation.",
7749
- transcriptPhaseCleanup,
7750
- );
7751
- try {
7752
- deleted = await validated.storage.deleteSessionVerified({
8423
+ if (!transcriptParentMatchesPersistedIdentity())
8424
+ return fail(
8425
+ "cleanup_pending",
8426
+ "Saved session cleanup is pending because transcript parent identity changed before exact mutation.",
8427
+ transcriptPhaseCleanup,
8428
+ );
8429
+ try {
8430
+ deleted = await validated.storage.deleteSessionVerified(
8431
+ {
7753
8432
  ...cleanupTarget,
8433
+ ...storageOwnerFields(),
7754
8434
  expectedArtifactsIdentity: undefined,
7755
8435
  detachedArtifactsPath: undefined,
7756
8436
  artifactsRemoved: true,
7757
- });
7758
- } catch (error) {
7759
- if (error instanceof SessionDeleteVerificationError && error.kind === "artifacts")
7760
- return await publishCanonicalArtifactReappearance(transcriptPhaseCleanup);
7761
- if (error instanceof SessionDeleteVerificationError)
7762
- return fail(
7763
- "invalid_input",
7764
- `Saved session deletion verification failed (${error.kind}): ${error.message}`,
7765
- );
8437
+ },
8438
+ validated.inspectProtocol,
8439
+ );
8440
+ } catch (error) {
8441
+ if (error instanceof Error && error.message.startsWith("task_artifact_owner_"))
8442
+ return await publishTaskArtifactOwnerPending(error, transcriptPhaseCleanup);
8443
+ if (error instanceof SessionDeleteVerificationError && error.kind === "artifacts")
8444
+ return await publishCanonicalArtifactReappearance(transcriptPhaseCleanup);
8445
+ if (error instanceof SessionDeleteVerificationError)
7766
8446
  return fail(
7767
- "unavailable",
7768
- `Unable to delete saved session transcript: ${error instanceof Error ? error.message : String(error)}`,
8447
+ "invalid_input",
8448
+ `Saved session deletion verification failed (${error.kind}): ${error.message}`,
7769
8449
  );
7770
- }
8450
+ if (freshPayloadRetirementForThisAttempt)
8451
+ return await publishTaskArtifactOwnerPending(error, transcriptPhaseCleanup);
8452
+ return fail(
8453
+ "unavailable",
8454
+ `Unable to delete saved session transcript: ${error instanceof Error ? error.message : String(error)}`,
8455
+ );
7771
8456
  }
7772
8457
  if (deleted.kind === "deleted" && !pathIsAbsent(canonicalArtifactsPath))
7773
8458
  return await publishCanonicalArtifactReappearance(transcriptPhaseCleanup);
@@ -7783,13 +8468,47 @@ async function executeLifecycleResponse(
7783
8468
  "Saved session cleanup is pending in artifacts: a planned quarantine alias remains before terminal completion.",
7784
8469
  );
7785
8470
  const retainedRootArtifactsPlan = durableArtifactsPlan;
7786
- if (deleted.kind === "cleanup_pending")
7787
- return fail(
8471
+ if (deleted.kind === "cleanup_pending") {
8472
+ if (deleted.phase === "task_artifact_owner" && !deleted.artifactsRemoved)
8473
+ return await publishTaskArtifactOwnerPending(deleted.error.message);
8474
+ const ownerEvidence =
8475
+ deleted.taskArtifactOwnerDeletionEvidence ?? cleanupTarget.taskArtifactOwnerDeletionEvidence;
8476
+ const ownerRetired = deleted.taskArtifactOwnerRetired ?? cleanupTarget.taskArtifactOwnerRetired;
8477
+ const ownerPayloadRetired =
8478
+ deleted.taskArtifactOwnerPayloadRetired ?? cleanupTarget.taskArtifactOwnerPayloadRetired;
8479
+ const ownerNamespaceRetained =
8480
+ deleted.taskArtifactOwnerNamespaceRetained ?? cleanupTarget.taskArtifactOwnerNamespaceRetained;
8481
+ const retainedOwnerNamespace = ownerPayloadRetired === true && ownerNamespaceRetained === true;
8482
+ const ownerContinuation =
8483
+ deleted.taskArtifactOwnerRetirementContinuation ?? cleanupTarget.taskArtifactOwnerRetirementContinuation;
8484
+ const ownerOutcome = deleted.taskArtifactOwnerRetirementOutcome
8485
+ ? taskArtifactOwnerRetirementDisposition(deleted.taskArtifactOwnerRetirementOutcome)
8486
+ : ownerRetirementOutcome;
8487
+ const transcriptDeleted =
8488
+ deleted.taskArtifactOwnerTranscriptDeleted === true || ownerTranscriptDeleted === true;
8489
+ const ownerError =
8490
+ deleted.phase === "task_artifact_owner" || deleted.error.message.startsWith("task_artifact_owner_")
8491
+ ? taskArtifactOwnerFailureDiagnostic(deleted.error)
8492
+ : undefined;
8493
+ const publicPhase =
8494
+ retainedOwnerNamespace || deleted.phase === "task_artifact_owner" ? "artifacts" : deleted.phase;
8495
+ const pending = fail(
7788
8496
  "cleanup_pending",
7789
- `Saved session cleanup is pending in ${deleted.phase}: ${deleted.error.message}`,
8497
+ `Saved session cleanup is pending in ${publicPhase}: ${deleted.error.message}`,
7790
8498
  {
7791
8499
  cleanupReceiptVersion: 1,
7792
- phase: deleted.phase,
8500
+ phase: publicPhase,
8501
+ ...(ownerEvidence && (deleted.phase === "task_artifact_owner" || retainedOwnerNamespace)
8502
+ ? { artifactsRemoved: true }
8503
+ : {}),
8504
+ ...(ownerEvidence ? { taskArtifactOwnerDeletionEvidence: ownerEvidence } : {}),
8505
+ ...(ownerContinuation ? { taskArtifactOwnerRetirementContinuation: ownerContinuation } : {}),
8506
+ ...(ownerOutcome ? { taskArtifactOwnerRetirementOutcome: ownerOutcome } : {}),
8507
+ ...(ownerPayloadRetired ? { taskArtifactOwnerPayloadRetired: true as const } : {}),
8508
+ ...(ownerNamespaceRetained ? { taskArtifactOwnerNamespaceRetained: true as const } : {}),
8509
+ ...(ownerRetired ? { taskArtifactOwnerRetired: true as const } : {}),
8510
+ ...(transcriptDeleted ? { taskArtifactOwnerTranscriptDeleted: true as const } : {}),
8511
+ ...(ownerError ? { taskArtifactOwnerCleanupError: ownerError } : {}),
7793
8512
  sessionId: validated.target.sessionId,
7794
8513
  sessionsRoot: validated.target.sessionsRoot,
7795
8514
  transcriptPath: validated.target.transcriptPath,
@@ -7873,6 +8592,34 @@ async function executeLifecycleResponse(
7873
8592
  : {}),
7874
8593
  },
7875
8594
  );
8595
+ await broker.ledger.transition(identity, "effect_started", { intendedSessionId: id, response: pending });
8596
+ return pending;
8597
+ }
8598
+ if (
8599
+ deleted.kind === "deleted" &&
8600
+ cleanupTarget.taskArtifactOwnerPayloadRetired === true &&
8601
+ cleanupTarget.taskArtifactOwnerNamespaceRetained === true
8602
+ ) {
8603
+ if (!logicalTranscriptRetirementMatches())
8604
+ return await publishTaskArtifactOwnerPending(
8605
+ "task_artifact_owner_transcript_disposition_mismatch",
8606
+ preauthorizedCleanup,
8607
+ );
8608
+ if (record) await appendSessionDeletedEvidence(broker, record);
8609
+ const residual = fail(
8610
+ "cleanup_pending",
8611
+ "Transcript retirement is complete; the task-artifact owner namespace remains pending in artifacts.",
8612
+ {
8613
+ ...cleanupWithFreshOwnerPayload(preauthorizedCleanup),
8614
+ phase: "artifacts",
8615
+ artifactsRemoved: true,
8616
+ artifactsAbsentAtAuthorization: undefined,
8617
+ taskArtifactOwnerTranscriptDeleted: true,
8618
+ },
8619
+ );
8620
+ await broker.ledger.transition(identity, "effect_started", { intendedSessionId: id, response: residual });
8621
+ return residual;
8622
+ }
7876
8623
 
7877
8624
  const completion = { ok: true, result: { sessionId: id } } as const;
7878
8625
  if (metadataCleanup.lifecycleFiles?.length) {