@gajae-code/coding-agent 0.11.10 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -12,6 +12,7 @@ const STORE_HEADINGS: Record<GcStore, string> = {
12
12
  file_locks: "Config file-locks",
13
13
  tmux_sessions: "Tmux sessions",
14
14
  registry_entries: "Harness-root registry entries",
15
+ local_roots: "Session local roots",
15
16
  };
16
17
 
17
18
  function actionLabel(record: GcRecord): string {
@@ -19,7 +19,13 @@ import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
19
19
 
20
20
  import { buildGcReportText } from "./gc-render";
21
21
 
22
- export type GcStore = "harness_leases" | "team_workers" | "file_locks" | "tmux_sessions" | "registry_entries";
22
+ export type GcStore =
23
+ | "harness_leases"
24
+ | "team_workers"
25
+ | "file_locks"
26
+ | "tmux_sessions"
27
+ | "registry_entries"
28
+ | "local_roots";
23
29
 
24
30
  export const GC_STORES: readonly GcStore[] = [
25
31
  "harness_leases",
@@ -27,6 +33,7 @@ export const GC_STORES: readonly GcStore[] = [
27
33
  "file_locks",
28
34
  "tmux_sessions",
29
35
  "registry_entries",
36
+ "local_roots",
30
37
  ] as const;
31
38
 
32
39
  /** Why a probed pid is kept instead of treated as dead. */
@@ -452,11 +459,13 @@ export async function defaultGcAdapters(): Promise<GcStoreAdapter[]> {
452
459
  { fileLocksGcAdapter },
453
460
  { teamWorkersGcAdapter },
454
461
  { tmuxSessionsGcAdapter },
462
+ { localRootsGcAdapter },
455
463
  ] = await Promise.all([
456
464
  import("../harness-control-plane/gc-adapter"),
457
465
  import("../config/file-lock-gc"),
458
466
  import("./team-gc"),
459
467
  import("./tmux-gc"),
468
+ import("../internal-urls/local-root-gc"),
460
469
  ]);
461
470
  return [
462
471
  harnessLeasesGcAdapter,
@@ -464,5 +473,6 @@ export async function defaultGcAdapters(): Promise<GcStoreAdapter[]> {
464
473
  fileLocksGcAdapter,
465
474
  tmuxSessionsGcAdapter,
466
475
  registryEntriesGcAdapter,
476
+ localRootsGcAdapter,
467
477
  ];
468
478
  }
@@ -1,26 +1,30 @@
1
1
  /**
2
- * Shared helpers for reading Linux `/proc/<pid>/stat` process start time.
2
+ * Shared helpers for reading Linux `/proc/<pid>/stat` process identity fields.
3
3
  *
4
4
  * The `comm` field (field 2, wrapped in parentheses) may itself contain spaces
5
5
  * and parentheses, so the only robust anchor is the *last* `)` in the stat
6
6
  * string. Field 22 (the process start time in clock ticks since boot) is the
7
- * 20th whitespace-separated token after that closing paren (index 19).
8
- *
9
- * Every caller previously parsed this format independently, with subtly
10
- * different failure handling. This module fails closed: any malformed input
11
- * (missing `)`, non-numeric field 22, unreadable `/proc` file, non-Linux
12
- * platform) yields `null` rather than an inconsistent sentinel.
7
+ * 20th whitespace-separated token after that closing paren (index 19). Field 7
8
+ * (`tty_nr`) is the 5th token after the closing paren (index 4).
13
9
  */
14
10
 
15
11
  import * as nodeFsSync from "node:fs";
16
12
  import * as nodeFs from "node:fs/promises";
17
13
 
18
- /**
19
- * Parse field 22 (process start time, in clock ticks since boot) from one
20
- * `/proc/<pid>/stat` record. Returns the raw numeric token, or `null` when the
21
- * record shape is malformed or field 22 is absent/non-numeric.
22
- */
23
- export function parseLinuxProcStartTime(stat: string | null | undefined): string | null {
14
+ export interface LinuxProcStatIdentity {
15
+ startTime: string;
16
+ ttyDevice: string;
17
+ }
18
+
19
+ export type LinuxProcPidProbeResult =
20
+ | ({ kind: "live" } & LinuxProcStatIdentity)
21
+ | { kind: "absent" }
22
+ | {
23
+ kind: "unverifiable";
24
+ reason: "unsupported_platform" | "invalid_pid" | "permission_denied" | "read_error" | "malformed_stat";
25
+ };
26
+
27
+ function parseLinuxProcIdentity(stat: string | null | undefined): LinuxProcStatIdentity | null {
24
28
  if (!stat || stat.includes("\0") || stat.includes("\r")) return null;
25
29
  const record = stat.endsWith("\n") ? stat.slice(0, -1) : stat;
26
30
  if (!record || record.includes("\n")) return null;
@@ -32,40 +36,70 @@ export function parseLinuxProcStartTime(stat: string | null | undefined): string
32
36
  const suffix = record.slice(close + 1);
33
37
  if (!/^[ \t]+/.test(suffix)) return null;
34
38
  const fields = suffix.trim().split(/[ \t]+/);
35
- if (fields.length < 20 || !/^[RSDTtXZPI]$/.test(fields[0])) return null;
36
-
39
+ if (fields.length < 20 || !/^[A-Za-z]$/.test(fields[0])) return null;
40
+ const ttyDevice = fields[4];
37
41
  const startTime = fields[19];
38
- return /^\d+$/.test(startTime) ? startTime : null;
42
+ if (!ttyDevice || !/^-?\d+$/.test(ttyDevice) || !/^\d+$/.test(startTime)) return null;
43
+ return { startTime, ttyDevice };
39
44
  }
40
45
 
41
- /**
42
- * Read `/proc/<pid>/stat` synchronously and return the parsed start time.
43
- * Returns `null` on non-Linux platforms, unreadable files, or malformed input.
44
- */
45
- export function readLinuxProcStartTimeSync(pid: number): string | null {
46
- if (process.platform !== "linux") return null;
47
- if (!Number.isSafeInteger(pid) || pid <= 0) return null;
46
+ function classifyProcReadError(error: unknown): Extract<LinuxProcPidProbeResult, { kind: "absent" | "unverifiable" }> {
47
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
48
+ if (code === "ENOENT" || code === "ESRCH") return { kind: "absent" };
49
+ if (code === "EACCES" || code === "EPERM") return { kind: "unverifiable", reason: "permission_denied" };
50
+ return { kind: "unverifiable", reason: "read_error" };
51
+ }
52
+
53
+ /** Parse field 22 (start time) from a `/proc/<pid>/stat` record. */
54
+ export function parseLinuxProcStartTime(stat: string | null | undefined): string | null {
55
+ return parseLinuxProcIdentity(stat)?.startTime ?? null;
56
+ }
57
+
58
+ /** Parse field 7 (`tty_nr`) from a `/proc/<pid>/stat` record. */
59
+ export function parseLinuxProcTtyDevice(stat: string | null | undefined): string | null {
60
+ return parseLinuxProcIdentity(stat)?.ttyDevice ?? null;
61
+ }
62
+
63
+ export function probeLinuxProcPidSync(pid: number): LinuxProcPidProbeResult {
64
+ if (!Number.isSafeInteger(pid) || pid <= 0) return { kind: "unverifiable", reason: "invalid_pid" };
65
+ if (process.platform !== "linux") return { kind: "unverifiable", reason: "unsupported_platform" };
48
66
  let stat: string;
49
67
  try {
50
68
  stat = nodeFsSync.readFileSync(`/proc/${pid}/stat`, "utf8");
51
- } catch {
52
- return null;
69
+ } catch (error) {
70
+ return classifyProcReadError(error);
53
71
  }
54
- return parseLinuxProcStartTime(stat);
72
+ const identity = parseLinuxProcIdentity(stat);
73
+ return identity ? { kind: "live", ...identity } : { kind: "unverifiable", reason: "malformed_stat" };
55
74
  }
56
75
 
57
- /**
58
- * Read `/proc/<pid>/stat` asynchronously and return the parsed start time.
59
- * Returns `null` on non-Linux platforms, unreadable files, or malformed input.
60
- */
61
- export async function readLinuxProcStartTime(pid: number): Promise<string | null> {
62
- if (process.platform !== "linux") return null;
63
- if (!Number.isSafeInteger(pid) || pid <= 0) return null;
76
+ export async function probeLinuxProcPid(pid: number): Promise<LinuxProcPidProbeResult> {
77
+ if (!Number.isSafeInteger(pid) || pid <= 0) return { kind: "unverifiable", reason: "invalid_pid" };
78
+ if (process.platform !== "linux") return { kind: "unverifiable", reason: "unsupported_platform" };
64
79
  let stat: string;
65
80
  try {
66
81
  stat = await nodeFs.readFile(`/proc/${pid}/stat`, "utf8");
67
- } catch {
68
- return null;
82
+ } catch (error) {
83
+ return classifyProcReadError(error);
69
84
  }
70
- return parseLinuxProcStartTime(stat);
85
+ const identity = parseLinuxProcIdentity(stat);
86
+ return identity ? { kind: "live", ...identity } : { kind: "unverifiable", reason: "malformed_stat" };
87
+ }
88
+
89
+ /**
90
+ * Read `/proc/<pid>/stat` synchronously and return the parsed start time.
91
+ * Returns `null` when the probe is absent or unverifiable.
92
+ */
93
+ export function readLinuxProcStartTimeSync(pid: number): string | null {
94
+ const probe = probeLinuxProcPidSync(pid);
95
+ return probe.kind === "live" ? probe.startTime : null;
96
+ }
97
+
98
+ /**
99
+ * Read `/proc/<pid>/stat` asynchronously and return the parsed start time.
100
+ * Returns `null` when the probe is absent or unverifiable.
101
+ */
102
+ export async function readLinuxProcStartTime(pid: number): Promise<string | null> {
103
+ const probe = await probeLinuxProcPid(pid);
104
+ return probe.kind === "live" ? probe.startTime : null;
71
105
  }
@@ -0,0 +1,389 @@
1
+ import { Database } from "bun:sqlite";
2
+ import * as fs from "node:fs/promises";
3
+ import { type LinuxProcPidProbeResult, probeLinuxProcPid } from "./linux-proc";
4
+ import { assertSafePathComponent } from "./session-layout";
5
+ import { memoryGuardClaimPaths } from "./tmux-owner-isolation";
6
+
7
+ const MEMORY_GUARD_CLAIM_RESOURCES = ["writer", "tty"] as const;
8
+ const MEMORY_GUARD_SQLITE_BUSY_TIMEOUT_MS = 5_000;
9
+ type MemoryGuardClaimResource = (typeof MEMORY_GUARD_CLAIM_RESOURCES)[number];
10
+
11
+ export interface MemoryGuardClaimOwner {
12
+ sessionId: string;
13
+ generation: string;
14
+ runId: string;
15
+ childToken: string;
16
+ pid: number;
17
+ processStartTime: string;
18
+ ttyDevice: string;
19
+ }
20
+
21
+ export interface MemoryGuardClaimsLease {
22
+ writerEpoch: number;
23
+ ttyEpoch: number;
24
+ owner: MemoryGuardClaimOwner;
25
+ claimStorePath: string;
26
+ }
27
+
28
+ const issuedMemoryGuardClaimsLeases = new WeakSet<MemoryGuardClaimsLease>();
29
+
30
+ function issueMemoryGuardClaimsLease(lease: MemoryGuardClaimsLease): MemoryGuardClaimsLease {
31
+ const owner = Object.freeze({ ...lease.owner });
32
+ const issued = Object.freeze({ ...lease, owner });
33
+ issuedMemoryGuardClaimsLeases.add(issued);
34
+ return issued;
35
+ }
36
+
37
+ export function isMemoryGuardClaimsLease(value: unknown): value is MemoryGuardClaimsLease {
38
+ return (
39
+ typeof value === "object" && value !== null && issuedMemoryGuardClaimsLeases.has(value as MemoryGuardClaimsLease)
40
+ );
41
+ }
42
+ export function isMemoryGuardClaimsLeaseForStateDir(value: unknown, stateDir: string): value is MemoryGuardClaimsLease {
43
+ return (
44
+ isMemoryGuardClaimsLease(value) &&
45
+ value.claimStorePath === memoryGuardClaimPaths(stateDir, value.owner.sessionId).databaseFile
46
+ );
47
+ }
48
+
49
+ export type MemoryGuardClaimsReleasedProof = MemoryGuardClaimsLease;
50
+
51
+ interface PersistedMemoryGuardClaimRow {
52
+ resource: MemoryGuardClaimResource;
53
+ epoch: number;
54
+ session_id: string;
55
+ generation: string;
56
+ run_id: string;
57
+ child_token: string;
58
+ pid: number;
59
+ process_start_time: string;
60
+ tty_device: string;
61
+ acquired_at: string;
62
+ }
63
+
64
+ export interface MemoryGuardOwnerClaimsDeps {
65
+ probePid(pid: number): Promise<LinuxProcPidProbeResult>;
66
+ now(): string;
67
+ }
68
+
69
+ const defaultDeps: MemoryGuardOwnerClaimsDeps = {
70
+ probePid,
71
+ now: () => new Date().toISOString(),
72
+ };
73
+
74
+ function probePid(pid: number): Promise<LinuxProcPidProbeResult> {
75
+ return probeLinuxProcPid(pid);
76
+ }
77
+
78
+ function assertClaimOwner(owner: MemoryGuardClaimOwner): void {
79
+ for (const [value, label] of [
80
+ [owner.sessionId, "memory guard session id"],
81
+ [owner.generation, "memory guard generation"],
82
+ [owner.runId, "memory guard run id"],
83
+ [owner.childToken, "memory guard child token"],
84
+ ] as const) {
85
+ assertSafePathComponent(value, label);
86
+ }
87
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) throw new Error("memory_guard_claim_invalid_pid");
88
+ if (!/^\d+$/.test(owner.processStartTime)) throw new Error("memory_guard_claim_invalid_process_start_time");
89
+ if (!/^-?\d+$/.test(owner.ttyDevice)) throw new Error("memory_guard_claim_invalid_tty_device");
90
+ }
91
+
92
+ async function prepareClaimDirectory(root: string): Promise<void> {
93
+ await fs.mkdir(root, { recursive: true, mode: 0o700 });
94
+ await fs.chmod(root, 0o700);
95
+ }
96
+
97
+ async function enforceDatabaseModes(databaseFile: string): Promise<void> {
98
+ for (const file of [databaseFile, `${databaseFile}-wal`, `${databaseFile}-shm`]) {
99
+ await fs.chmod(file, 0o600).catch(() => undefined);
100
+ }
101
+ }
102
+
103
+ function configureClaimsDatabase(database: Database): void {
104
+ database.exec("PRAGMA journal_mode = WAL");
105
+ database.exec("PRAGMA synchronous = FULL");
106
+ database.exec(`PRAGMA busy_timeout = ${MEMORY_GUARD_SQLITE_BUSY_TIMEOUT_MS}`);
107
+ database.exec(`
108
+ CREATE TABLE IF NOT EXISTS meta(
109
+ key TEXT PRIMARY KEY,
110
+ value TEXT NOT NULL
111
+ );
112
+ INSERT OR IGNORE INTO meta(key, value) VALUES ('epoch', '0');
113
+ CREATE TABLE IF NOT EXISTS claims(
114
+ resource TEXT PRIMARY KEY CHECK(resource IN ('writer', 'tty')),
115
+ epoch INTEGER NOT NULL,
116
+ session_id TEXT NOT NULL,
117
+ generation TEXT NOT NULL,
118
+ run_id TEXT NOT NULL,
119
+ child_token TEXT NOT NULL,
120
+ pid INTEGER NOT NULL,
121
+ process_start_time TEXT NOT NULL,
122
+ tty_device TEXT NOT NULL,
123
+ acquired_at TEXT NOT NULL
124
+ );
125
+ `);
126
+ }
127
+
128
+ async function openClaimsDatabase(
129
+ stateDir: string,
130
+ sessionId: string,
131
+ ): Promise<{ database: Database; databaseFile: string }> {
132
+ assertSafePathComponent(sessionId, "memory guard session id");
133
+ const paths = memoryGuardClaimPaths(stateDir, sessionId);
134
+ await prepareClaimDirectory(paths.root);
135
+ const database = new Database(paths.databaseFile, { create: true });
136
+ configureClaimsDatabase(database);
137
+ await enforceDatabaseModes(paths.databaseFile);
138
+ return { database, databaseFile: paths.databaseFile };
139
+ }
140
+
141
+ function readEpoch(database: Database): number {
142
+ const row = database.prepare("SELECT value FROM meta WHERE key = 'epoch'").get() as { value: string } | null;
143
+ if (!row || !/^\d+$/.test(row.value)) throw new Error("memory_guard_claim_epoch_invalid");
144
+ const value = Number(row.value);
145
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("memory_guard_claim_epoch_invalid");
146
+ return value;
147
+ }
148
+
149
+ function allocateEpoch(database: Database): number {
150
+ const next = readEpoch(database) + 1;
151
+ if (!Number.isSafeInteger(next) || next <= 0) throw new Error("memory_guard_claim_epoch_overflow");
152
+ database.prepare("UPDATE meta SET value = ? WHERE key = 'epoch'").run(String(next));
153
+ return next;
154
+ }
155
+
156
+ function readClaimRows(database: Database): PersistedMemoryGuardClaimRow[] {
157
+ const rows = database
158
+ .prepare(
159
+ "SELECT resource, epoch, session_id, generation, run_id, child_token, pid, process_start_time, tty_device, acquired_at FROM claims ORDER BY resource ASC",
160
+ )
161
+ .all() as PersistedMemoryGuardClaimRow[];
162
+ for (const row of rows) {
163
+ if (!MEMORY_GUARD_CLAIM_RESOURCES.includes(row.resource)) throw new Error("memory_guard_claim_resource_invalid");
164
+ if (!Number.isSafeInteger(row.epoch) || row.epoch <= 0) throw new Error("memory_guard_claim_epoch_invalid");
165
+ }
166
+ return rows;
167
+ }
168
+
169
+ async function assertLiveOwner(owner: MemoryGuardClaimOwner, deps: MemoryGuardOwnerClaimsDeps): Promise<void> {
170
+ const probe = await deps.probePid(owner.pid);
171
+ if (probe.kind !== "live")
172
+ throw new Error(
173
+ `memory_guard_claim_owner_unverifiable:${probe.kind === "unverifiable" ? probe.reason : "absent"}`,
174
+ );
175
+ if (probe.startTime !== owner.processStartTime) throw new Error("memory_guard_claim_owner_start_time_mismatch");
176
+ if (probe.ttyDevice !== owner.ttyDevice) throw new Error("memory_guard_claim_owner_tty_mismatch");
177
+ }
178
+
179
+ async function classifyExistingRow(
180
+ row: PersistedMemoryGuardClaimRow,
181
+ deps: MemoryGuardOwnerClaimsDeps,
182
+ ): Promise<"live" | "reclaim"> {
183
+ const probe = await deps.probePid(row.pid);
184
+ if (probe.kind === "absent") return "reclaim";
185
+ if (probe.kind === "live") return probe.startTime === row.process_start_time ? "live" : "reclaim";
186
+ throw new Error(`memory_guard_claim_existing_owner_unverifiable:${probe.reason}`);
187
+ }
188
+
189
+ function claimRowsEqual(left: PersistedMemoryGuardClaimRow[], right: PersistedMemoryGuardClaimRow[]): boolean {
190
+ return JSON.stringify(left) === JSON.stringify(right);
191
+ }
192
+
193
+ async function inspectExistingRows(
194
+ rows: PersistedMemoryGuardClaimRow[],
195
+ deps: MemoryGuardOwnerClaimsDeps,
196
+ liveError: (resource: MemoryGuardClaimResource) => Error,
197
+ ): Promise<void> {
198
+ for (const row of rows) {
199
+ const state = await classifyExistingRow(row, deps);
200
+ if (state === "live") throw liveError(row.resource);
201
+ }
202
+ }
203
+
204
+ function insertClaimRow(
205
+ database: Database,
206
+ resource: MemoryGuardClaimResource,
207
+ epoch: number,
208
+ owner: MemoryGuardClaimOwner,
209
+ acquiredAt: string,
210
+ ): void {
211
+ database
212
+ .prepare(
213
+ "INSERT INTO claims(resource, epoch, session_id, generation, run_id, child_token, pid, process_start_time, tty_device, acquired_at) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
214
+ )
215
+ .run(
216
+ resource,
217
+ epoch,
218
+ owner.sessionId,
219
+ owner.generation,
220
+ owner.runId,
221
+ owner.childToken,
222
+ owner.pid,
223
+ owner.processStartTime,
224
+ owner.ttyDevice,
225
+ acquiredAt,
226
+ );
227
+ }
228
+
229
+ function deleteExactClaimRow(
230
+ database: Database,
231
+ resource: MemoryGuardClaimResource,
232
+ epoch: number,
233
+ owner: MemoryGuardClaimOwner,
234
+ ): number {
235
+ return database
236
+ .prepare(
237
+ "DELETE FROM claims WHERE resource = ? AND epoch = ? AND session_id = ? AND generation = ? AND run_id = ? AND child_token = ? AND pid = ? AND process_start_time = ? AND tty_device = ?",
238
+ )
239
+ .run(
240
+ resource,
241
+ epoch,
242
+ owner.sessionId,
243
+ owner.generation,
244
+ owner.runId,
245
+ owner.childToken,
246
+ owner.pid,
247
+ owner.processStartTime,
248
+ owner.ttyDevice,
249
+ ).changes;
250
+ }
251
+
252
+ function rollbackQuietly(database: Database): void {
253
+ try {
254
+ database.exec("ROLLBACK");
255
+ } catch {
256
+ return;
257
+ }
258
+ }
259
+
260
+ export function memoryGuardClaimsDatabaseFile(stateDir: string, sessionId: string): string {
261
+ return memoryGuardClaimPaths(stateDir, sessionId).databaseFile;
262
+ }
263
+
264
+ export async function acquireMemoryGuardClaims(
265
+ stateDir: string,
266
+ owner: MemoryGuardClaimOwner,
267
+ deps: MemoryGuardOwnerClaimsDeps = defaultDeps,
268
+ ): Promise<MemoryGuardClaimsLease> {
269
+ assertClaimOwner(owner);
270
+ await assertLiveOwner(owner, deps);
271
+ const { database, databaseFile } = await openClaimsDatabase(stateDir, owner.sessionId);
272
+ try {
273
+ for (let attempt = 0; attempt < 3; attempt += 1) {
274
+ const inspectedRows = readClaimRows(database);
275
+ await inspectExistingRows(
276
+ inspectedRows,
277
+ deps,
278
+ resource => new Error(`memory_guard_claim_live_contention:${resource}`),
279
+ );
280
+ database.exec("BEGIN IMMEDIATE");
281
+ try {
282
+ if (!claimRowsEqual(inspectedRows, readClaimRows(database))) {
283
+ database.exec("ROLLBACK");
284
+ continue;
285
+ }
286
+ database.exec("DELETE FROM claims");
287
+ const acquiredAt = deps.now();
288
+ const writerEpoch = allocateEpoch(database);
289
+ insertClaimRow(database, "writer", writerEpoch, owner, acquiredAt);
290
+ const ttyEpoch = allocateEpoch(database);
291
+ insertClaimRow(database, "tty", ttyEpoch, owner, acquiredAt);
292
+ database.exec("COMMIT");
293
+ await enforceDatabaseModes(databaseFile);
294
+ return issueMemoryGuardClaimsLease({ writerEpoch, ttyEpoch, owner, claimStorePath: databaseFile });
295
+ } catch (error) {
296
+ rollbackQuietly(database);
297
+ throw error;
298
+ }
299
+ }
300
+ throw new Error("memory_guard_claim_rows_changed");
301
+ } finally {
302
+ database.close();
303
+ }
304
+ }
305
+
306
+ export async function releaseMemoryGuardClaims(stateDir: string, claim: MemoryGuardClaimsLease): Promise<void> {
307
+ if (!issuedMemoryGuardClaimsLeases.has(claim)) throw new Error("memory_guard_claim_lease_invalid");
308
+ assertClaimOwner(claim.owner);
309
+ const expectedDatabaseFile = memoryGuardClaimPaths(stateDir, claim.owner.sessionId).databaseFile;
310
+ if (claim.claimStorePath !== expectedDatabaseFile) throw new Error("memory_guard_claim_store_mismatch");
311
+ const { database } = await openClaimsDatabase(stateDir, claim.owner.sessionId);
312
+ try {
313
+ database.exec("BEGIN IMMEDIATE");
314
+ const writerChanges = deleteExactClaimRow(database, "writer", claim.writerEpoch, claim.owner);
315
+ const ttyChanges = deleteExactClaimRow(database, "tty", claim.ttyEpoch, claim.owner);
316
+ if (writerChanges !== 1 || ttyChanges !== 1) throw new Error("memory_guard_claim_release_mismatch");
317
+ database.exec("COMMIT");
318
+ issuedMemoryGuardClaimsLeases.delete(claim);
319
+ } catch (error) {
320
+ rollbackQuietly(database);
321
+ throw error;
322
+ } finally {
323
+ database.close();
324
+ }
325
+ }
326
+
327
+ export async function probeMemoryGuardClaimsReleased(
328
+ stateDir: string,
329
+ owner: MemoryGuardClaimOwner,
330
+ deps: MemoryGuardOwnerClaimsDeps = defaultDeps,
331
+ ): Promise<MemoryGuardClaimsReleasedProof> {
332
+ assertClaimOwner(owner);
333
+ await assertLiveOwner(owner, deps);
334
+ const { database, databaseFile } = await openClaimsDatabase(stateDir, owner.sessionId);
335
+ try {
336
+ for (let attempt = 0; attempt < 3; attempt += 1) {
337
+ const inspectedRows = readClaimRows(database);
338
+ await inspectExistingRows(
339
+ inspectedRows,
340
+ deps,
341
+ resource => new Error(`memory_guard_claims_still_live:${resource}`),
342
+ );
343
+ database.exec("BEGIN IMMEDIATE");
344
+ try {
345
+ if (!claimRowsEqual(inspectedRows, readClaimRows(database))) {
346
+ database.exec("ROLLBACK");
347
+ continue;
348
+ }
349
+ database.exec("DELETE FROM claims");
350
+ const acquiredAt = deps.now();
351
+ const writerEpoch = allocateEpoch(database);
352
+ insertClaimRow(database, "writer", writerEpoch, owner, acquiredAt);
353
+ const ttyEpoch = allocateEpoch(database);
354
+ insertClaimRow(database, "tty", ttyEpoch, owner, acquiredAt);
355
+ database.exec("COMMIT");
356
+ await enforceDatabaseModes(databaseFile);
357
+ const proof = issueMemoryGuardClaimsLease({ writerEpoch, ttyEpoch, owner, claimStorePath: databaseFile });
358
+ await releaseMemoryGuardClaims(stateDir, proof);
359
+ return proof;
360
+ } catch (error) {
361
+ rollbackQuietly(database);
362
+ throw error;
363
+ }
364
+ }
365
+ throw new Error("memory_guard_claim_rows_changed");
366
+ } finally {
367
+ database.close();
368
+ }
369
+ }
370
+
371
+ export interface PersistedMemoryGuardClaimsSnapshot {
372
+ epoch: number;
373
+ claims: PersistedMemoryGuardClaimRow[];
374
+ }
375
+
376
+ export async function readMemoryGuardClaimsForTest(
377
+ stateDir: string,
378
+ sessionId: string,
379
+ ): Promise<PersistedMemoryGuardClaimsSnapshot> {
380
+ const { database } = await openClaimsDatabase(stateDir, sessionId);
381
+ try {
382
+ return {
383
+ epoch: readEpoch(database),
384
+ claims: readClaimRows(database),
385
+ };
386
+ } finally {
387
+ database.close();
388
+ }
389
+ }