@gajae-code/coding-agent 0.8.1 → 0.9.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 (253) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/completion-cli.d.ts +41 -0
  5. package/dist/types/cli/config-cli.d.ts +1 -0
  6. package/dist/types/cli/fixture-report.d.ts +20 -0
  7. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  8. package/dist/types/cli/nofile-limit.d.ts +11 -0
  9. package/dist/types/cli/notify-cli.d.ts +7 -0
  10. package/dist/types/cli/update-cli.d.ts +22 -0
  11. package/dist/types/cli.d.ts +124 -1
  12. package/dist/types/commands/completion.d.ts +29 -0
  13. package/dist/types/commands/config.d.ts +3 -0
  14. package/dist/types/commands/deep-interview.d.ts +3 -0
  15. package/dist/types/config/file-lock.d.ts +7 -6
  16. package/dist/types/config/keybindings.d.ts +2 -2
  17. package/dist/types/config/model-profile-activation.d.ts +6 -0
  18. package/dist/types/config/settings-schema.d.ts +105 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  26. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  27. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/deep-interview-state.d.ts +17 -2
  29. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  30. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  31. package/dist/types/gjc-runtime/team-runtime.d.ts +28 -1
  32. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +120 -13
  34. package/dist/types/goals/runtime.d.ts +1 -0
  35. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  36. package/dist/types/main.d.ts +4 -1
  37. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  38. package/dist/types/modes/components/model-selector.d.ts +4 -0
  39. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  40. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  41. package/dist/types/modes/components/status-line.d.ts +9 -0
  42. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  43. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  45. package/dist/types/modes/interactive-mode.d.ts +2 -0
  46. package/dist/types/modes/print-mode.d.ts +13 -1
  47. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  48. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  49. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  50. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  51. package/dist/types/modes/types.d.ts +1 -0
  52. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  53. package/dist/types/notifications/config.d.ts +14 -1
  54. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  55. package/dist/types/notifications/index.d.ts +6 -3
  56. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  57. package/dist/types/notifications/telegram-daemon.d.ts +23 -3
  58. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  59. package/dist/types/notifications/threaded-render.d.ts +6 -0
  60. package/dist/types/runtime-api-key.d.ts +7 -0
  61. package/dist/types/sdk.d.ts +3 -0
  62. package/dist/types/session/agent-session.d.ts +53 -1
  63. package/dist/types/session/agent-storage.d.ts +1 -0
  64. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  65. package/dist/types/session/messages.d.ts +4 -0
  66. package/dist/types/session/session-manager.d.ts +17 -1
  67. package/dist/types/skill-state/active-state.d.ts +18 -1
  68. package/dist/types/system-prompt.d.ts +18 -2
  69. package/dist/types/task/executor.d.ts +3 -0
  70. package/dist/types/task/fork-context-budget.d.ts +7 -0
  71. package/dist/types/task/token-log.d.ts +30 -0
  72. package/dist/types/task/token-log.test.d.ts +1 -0
  73. package/dist/types/tools/bisect.d.ts +81 -0
  74. package/dist/types/tools/computer.d.ts +2 -2
  75. package/dist/types/tools/index.d.ts +1 -0
  76. package/dist/types/tools/search.d.ts +2 -2
  77. package/dist/types/utils/file-mentions.d.ts +14 -0
  78. package/dist/types/utils/git.d.ts +32 -0
  79. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  80. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  81. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  82. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  83. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  84. package/package.json +7 -7
  85. package/src/append-only-mode.ts +58 -0
  86. package/src/autoresearch/prompt.md +1 -1
  87. package/src/autoresearch/tools/log-experiment.ts +1 -1
  88. package/src/capability/rule.ts +9 -0
  89. package/src/cli/completion-cli.ts +234 -0
  90. package/src/cli/config-cli.ts +61 -9
  91. package/src/cli/fast-help.ts +1 -0
  92. package/src/cli/fixture-report.test.ts +230 -0
  93. package/src/cli/fixture-report.ts +171 -0
  94. package/src/cli/nofile-limit.ts +48 -0
  95. package/src/cli/notify-cli.ts +91 -7
  96. package/src/cli/update-cli.ts +85 -9
  97. package/src/cli.ts +29 -1
  98. package/src/commands/completion.ts +84 -0
  99. package/src/commands/config.ts +2 -0
  100. package/src/commands/contribution-prep.ts +1 -1
  101. package/src/commands/deep-interview.ts +2 -1
  102. package/src/config/file-lock.ts +73 -34
  103. package/src/config/keybindings.ts +2 -2
  104. package/src/config/model-profile-activation.ts +65 -0
  105. package/src/config/model-registry.ts +14 -2
  106. package/src/config/settings-schema.ts +68 -1
  107. package/src/config/settings.ts +22 -0
  108. package/src/coordinator-mcp/server.ts +34 -8
  109. package/src/defaults/gjc/skills/deep-interview/SKILL.md +50 -18
  110. package/src/defaults/gjc/skills/ralplan/SKILL.md +40 -99
  111. package/src/defaults/gjc/skills/team/SKILL.md +17 -14
  112. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  113. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  114. package/src/defaults/gjc-defaults.ts +9 -0
  115. package/src/discovery/helpers.ts +10 -0
  116. package/src/discovery/index.ts +1 -0
  117. package/src/export/html/template.generated.ts +1 -1
  118. package/src/export/html/template.js +60 -2
  119. package/src/export/ttsr.ts +96 -18
  120. package/src/extensibility/extensions/runner.ts +183 -219
  121. package/src/extensibility/extensions/types.ts +9 -0
  122. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  123. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  124. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  125. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  126. package/src/gjc-runtime/deep-interview-state.ts +44 -2
  127. package/src/gjc-runtime/session-state-sidecar.ts +263 -20
  128. package/src/gjc-runtime/state-runtime.ts +12 -2
  129. package/src/gjc-runtime/state-writer.ts +15 -0
  130. package/src/gjc-runtime/team-runtime.ts +187 -15
  131. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  132. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  133. package/src/gjc-runtime/ultragoal-runtime.ts +1663 -159
  134. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  135. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  136. package/src/goals/runtime.ts +10 -2
  137. package/src/hindsight/backend.ts +1 -3
  138. package/src/hooks/native-skill-hook.ts +124 -4
  139. package/src/internal-urls/docs-index.generated.ts +16 -7
  140. package/src/main.ts +95 -43
  141. package/src/memories/index.ts +12 -1
  142. package/src/modes/components/assistant-message.ts +241 -82
  143. package/src/modes/components/custom-editor.ts +1 -0
  144. package/src/modes/components/model-selector.ts +54 -11
  145. package/src/modes/components/queued-message-selector.ts +92 -0
  146. package/src/modes/components/settings-selector.ts +4 -0
  147. package/src/modes/components/status-line.ts +167 -54
  148. package/src/modes/components/tool-execution.ts +44 -19
  149. package/src/modes/components/welcome.ts +160 -44
  150. package/src/modes/controllers/command-controller.ts +40 -4
  151. package/src/modes/controllers/event-controller.ts +66 -47
  152. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  153. package/src/modes/controllers/input-controller.ts +180 -18
  154. package/src/modes/controllers/selector-controller.ts +122 -27
  155. package/src/modes/interactive-mode.ts +51 -16
  156. package/src/modes/print-mode.ts +43 -4
  157. package/src/modes/prompt-action-autocomplete.ts +21 -20
  158. package/src/modes/rpc/rpc-mode.ts +29 -3
  159. package/src/modes/rpc/rpc-types.ts +9 -0
  160. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  161. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  162. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  163. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  164. package/src/modes/types.ts +1 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  166. package/src/modes/utils/ui-helpers.ts +19 -6
  167. package/src/notifications/config.ts +25 -5
  168. package/src/notifications/daemon-paths.ts +2 -0
  169. package/src/notifications/index.ts +117 -14
  170. package/src/notifications/lifecycle-commands.ts +37 -7
  171. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  172. package/src/notifications/telegram-daemon-cli.ts +2 -2
  173. package/src/notifications/telegram-daemon-control.ts +5 -3
  174. package/src/notifications/telegram-daemon.ts +302 -51
  175. package/src/notifications/telegram-reference.ts +0 -16
  176. package/src/notifications/threaded-render.ts +17 -1
  177. package/src/prompts/agents/architect.md +19 -10
  178. package/src/prompts/agents/critic.md +16 -18
  179. package/src/prompts/agents/executor.md +14 -11
  180. package/src/prompts/agents/planner.md +18 -13
  181. package/src/prompts/ci-green-request.md +3 -8
  182. package/src/prompts/goals/goal-continuation.md +0 -5
  183. package/src/prompts/goals/goal-mode-active.md +0 -5
  184. package/src/prompts/system/agent-creation-architect.md +2 -2
  185. package/src/prompts/system/plan-mode-active.md +1 -1
  186. package/src/prompts/system/plan-mode-subagent.md +3 -3
  187. package/src/prompts/system/project-prompt.md +0 -13
  188. package/src/prompts/system/subagent-system-prompt.md +3 -2
  189. package/src/prompts/system/system-prompt.md +29 -19
  190. package/src/prompts/system/volatile-project-context.md +10 -0
  191. package/src/prompts/system/web-search.md +4 -7
  192. package/src/prompts/tools/apply-patch.md +6 -0
  193. package/src/prompts/tools/ast-edit.md +1 -0
  194. package/src/prompts/tools/bash.md +1 -1
  195. package/src/prompts/tools/bisect.md +22 -0
  196. package/src/prompts/tools/browser.md +1 -1
  197. package/src/prompts/tools/computer.md +2 -2
  198. package/src/prompts/tools/cron.md +0 -2
  199. package/src/prompts/tools/eval.md +2 -2
  200. package/src/prompts/tools/find.md +1 -1
  201. package/src/prompts/tools/github.md +6 -6
  202. package/src/prompts/tools/goal.md +1 -3
  203. package/src/prompts/tools/image-gen.md +12 -5
  204. package/src/prompts/tools/irc.md +5 -4
  205. package/src/prompts/tools/job.md +2 -2
  206. package/src/prompts/tools/lsp.md +1 -1
  207. package/src/prompts/tools/monitor.md +2 -2
  208. package/src/prompts/tools/patch.md +2 -2
  209. package/src/prompts/tools/replace.md +2 -21
  210. package/src/prompts/tools/rewind.md +1 -1
  211. package/src/prompts/tools/search-tool-bm25.md +1 -11
  212. package/src/prompts/tools/search.md +1 -1
  213. package/src/prompts/tools/skill.md +3 -2
  214. package/src/prompts/tools/ssh.md +2 -1
  215. package/src/prompts/tools/subagent.md +2 -2
  216. package/src/prompts/tools/task.md +4 -5
  217. package/src/prompts/tools/todo-write.md +1 -1
  218. package/src/prompts/tools/vim.md +1 -0
  219. package/src/prompts/tools/web-search.md +2 -0
  220. package/src/prompts/tools/write.md +8 -2
  221. package/src/runtime-api-key.ts +16 -0
  222. package/src/sdk.ts +52 -28
  223. package/src/session/agent-session.ts +951 -132
  224. package/src/session/agent-storage.ts +4 -0
  225. package/src/session/file-mention-pruning.ts +64 -0
  226. package/src/session/messages.ts +4 -0
  227. package/src/session/session-manager.ts +103 -4
  228. package/src/skill-state/active-state.ts +141 -5
  229. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  230. package/src/slash-commands/builtin-registry.ts +218 -71
  231. package/src/system-prompt.ts +57 -16
  232. package/src/task/executor.ts +132 -24
  233. package/src/task/fork-context-advisory.ts +2 -7
  234. package/src/task/fork-context-budget.ts +9 -0
  235. package/src/task/index.ts +13 -6
  236. package/src/task/output-manager.ts +20 -8
  237. package/src/task/receipt.ts +26 -7
  238. package/src/task/token-log.test.ts +188 -0
  239. package/src/task/token-log.ts +179 -0
  240. package/src/tools/bisect.ts +367 -0
  241. package/src/tools/computer.ts +246 -43
  242. package/src/tools/index.ts +3 -0
  243. package/src/tools/output-meta.ts +116 -4
  244. package/src/tools/search.ts +8 -2
  245. package/src/tools/skill.ts +15 -0
  246. package/src/tools/telegram-send.ts +6 -0
  247. package/src/utils/file-mentions.ts +39 -7
  248. package/src/utils/git.ts +73 -0
  249. package/src/web/scrapers/youtube.ts +24 -12
  250. package/src/workflow/workflow-intent-diff.ts +29 -6
  251. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  252. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  253. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -1,3 +1,4 @@
1
+ import type { Stats } from "node:fs";
1
2
  import * as fs from "node:fs/promises";
2
3
  import { isEnoent } from "@gajae-code/utils/fs-error";
3
4
 
@@ -22,9 +23,10 @@ function getLockPath(filePath: string): string {
22
23
  return `${filePath}.lock`;
23
24
  }
24
25
 
25
- async function writeLockInfo(lockPath: string): Promise<void> {
26
+ async function writeLockInfo(lockPath: string): Promise<LockInfo> {
26
27
  const info: LockInfo = { pid: process.pid, timestamp: Date.now() };
27
28
  await Bun.write(`${lockPath}/info`, JSON.stringify(info));
29
+ return info;
28
30
  }
29
31
 
30
32
  async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
@@ -51,17 +53,30 @@ export interface FileLockOwnerToken {
51
53
  timestamp: number;
52
54
  }
53
55
 
54
- /** Outcome of a guarded GC removal attempt (`removeFileLockDirForGc`). */
56
+ /** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
55
57
  export type FileLockGcRemoval = "removed" | "owner_changed" | "missing";
56
58
 
59
+ interface LockDirStatToken {
60
+ dev: number;
61
+ ino: number;
62
+ mtimeMs: number;
63
+ ctimeMs: number;
64
+ }
65
+
66
+ type LockStaleSnapshot =
67
+ | { stale: false }
68
+ | { stale: true; owner: FileLockOwnerToken }
69
+ | { stale: true; owner: null; stat: LockDirStatToken };
70
+
57
71
  /**
58
72
  * @internal
59
- * Fail-closed removal of a dead lock dir for GC. Re-reads the on-disk owner
60
- * token as close to the unlink as possible and only deletes the dir when it
61
- * STILL holds the exact `{pid, timestamp}` identity the caller observed dead.
73
+ * Fail-closed removal of a lock dir whose owner is expected to be dead or
74
+ * finished. Re-reads the on-disk owner token as close to the unlink as possible
75
+ * and only deletes the dir when it STILL holds the exact `{pid, timestamp}`
76
+ * identity the caller observed.
62
77
  *
63
- * Closes the prune-time TOCTOU window (#606): between GC's dead re-read/probe
64
- * and the unlink, a live process can reclaim a stale lock at the same path
78
+ * Closes stale-cleanup TOCTOU windows (#606): between a dead/stale re-read and
79
+ * the unlink, a live process can reclaim a stale lock at the same path
65
80
  * (`acquireLock` rms the stale dir, then re-`mkdir`s and rewrites `info` with a
66
81
  * fresh pid+timestamp). Deleting by path alone would reap that LIVE lock. Any
67
82
  * mismatch (`owner_changed`) or absent/unreadable info (`missing` — e.g. a
@@ -99,14 +114,28 @@ function ownerLiveness(pid: number): OwnerLiveness {
99
114
  }
100
115
  }
101
116
 
102
- async function isLockStale(lockPath: string, staleMs: number): Promise<boolean> {
117
+ function statToken(stats: Stats): LockDirStatToken {
118
+ return {
119
+ dev: stats.dev,
120
+ ino: stats.ino,
121
+ mtimeMs: stats.mtimeMs,
122
+ ctimeMs: stats.ctimeMs,
123
+ };
124
+ }
125
+
126
+ function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
127
+ return a.dev === b.dev && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
128
+ }
129
+
130
+ async function staleLockSnapshot(lockPath: string, staleMs: number): Promise<LockStaleSnapshot> {
103
131
  const info = await readLockInfo(lockPath);
104
132
  if (!info) {
105
133
  try {
106
134
  const stats = await fs.stat(lockPath);
107
- return Date.now() - stats.mtimeMs > staleMs;
135
+ if (Date.now() - stats.mtimeMs <= staleMs) return { stale: false };
136
+ return { stale: true, owner: null, stat: statToken(stats) };
108
137
  } catch (err) {
109
- if (isEnoent(err)) return false;
138
+ if (isEnoent(err)) return { stale: false };
110
139
  throw err;
111
140
  }
112
141
  }
@@ -115,53 +144,63 @@ async function isLockStale(lockPath: string, staleMs: number): Promise<boolean>
115
144
  // not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
116
145
  // liveness is indeterminate do we fall back to the staleMs elapsed-time heuristic.
117
146
  const liveness = ownerLiveness(info.pid);
118
- if (liveness === "dead") return true;
119
- if (liveness === "alive") return false;
120
- return Date.now() - info.timestamp > staleMs;
147
+ if (liveness === "alive") return { stale: false };
148
+ if (liveness === "dead" || Date.now() - info.timestamp > staleMs) {
149
+ return { stale: true, owner: { pid: info.pid, timestamp: info.timestamp } };
150
+ }
151
+ return { stale: false };
121
152
  }
122
153
 
123
- async function tryAcquireLock(lockPath: string): Promise<boolean> {
154
+ async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSnapshot): Promise<boolean> {
155
+ if (!snapshot.stale) return false;
156
+ if (snapshot.owner) {
157
+ return (await removeFileLockDirForGc(lockPath, snapshot.owner)) === "removed";
158
+ }
159
+
160
+ const currentInfo = await readLockInfo(lockPath);
161
+ if (currentInfo) return false;
124
162
  try {
125
- await fs.mkdir(lockPath);
126
- await writeLockInfo(lockPath);
163
+ const currentStats = await fs.stat(lockPath);
164
+ if (!sameStatToken(statToken(currentStats), snapshot.stat)) return false;
165
+ await fs.rm(lockPath, { recursive: true, force: true });
127
166
  return true;
167
+ } catch (err) {
168
+ if (isEnoent(err)) return false;
169
+ throw err;
170
+ }
171
+ }
172
+
173
+ async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
174
+ try {
175
+ await fs.mkdir(lockPath);
176
+ return await writeLockInfo(lockPath);
128
177
  } catch (error) {
129
178
  if ((error as NodeJS.ErrnoException).code === "EEXIST") {
130
- return false;
179
+ return null;
131
180
  }
132
181
  throw error;
133
182
  }
134
183
  }
135
184
 
136
- async function releaseLock(lockPath: string): Promise<void> {
185
+ async function releaseLock(lockPath: string, owner: FileLockOwnerToken): Promise<void> {
137
186
  try {
138
- await fs.rm(lockPath, { recursive: true });
187
+ await removeFileLockDirForGc(lockPath, owner);
139
188
  } catch {
140
189
  // Ignore errors on release
141
190
  }
142
191
  }
143
-
144
- async function lockExists(lockPath: string): Promise<boolean> {
145
- try {
146
- await fs.stat(lockPath);
147
- return true;
148
- } catch (err) {
149
- if (isEnoent(err)) return false;
150
- throw err;
151
- }
152
- }
153
-
154
192
  async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
155
193
  const opts = { ...DEFAULT_OPTIONS, ...options };
156
194
  const lockPath = getLockPath(filePath);
157
195
 
158
196
  for (let attempt = 0; attempt < opts.retries; attempt++) {
159
- if (await tryAcquireLock(lockPath)) {
160
- return () => releaseLock(lockPath);
197
+ const owner = await tryAcquireLock(lockPath);
198
+ if (owner) {
199
+ return () => releaseLock(lockPath, owner);
161
200
  }
162
201
 
163
- if ((await lockExists(lockPath)) && (await isLockStale(lockPath, opts.staleMs))) {
164
- await releaseLock(lockPath);
202
+ const stale = await staleLockSnapshot(lockPath, opts.staleMs);
203
+ if (await removeStaleLockForAcquire(lockPath, stale)) {
165
204
  continue;
166
205
  }
167
206
 
@@ -134,8 +134,8 @@ export const KEYBINDINGS = {
134
134
  description: "Queue message for next turn",
135
135
  },
136
136
  "app.message.dequeue": {
137
- defaultKeys: "alt+up",
138
- description: "Dequeue message",
137
+ defaultKeys: ["alt+up", "alt+down"],
138
+ description: "Select queued message to edit",
139
139
  },
140
140
  "app.clipboard.pasteImage": {
141
141
  defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v",
@@ -76,6 +76,34 @@ export interface MaterializeModelProfileAssignmentOptions {
76
76
  selector: string;
77
77
  }
78
78
 
79
+ export interface MaterializeModelProfileAssignmentsOptions {
80
+ session: Pick<
81
+ ModelProfileActivationSession,
82
+ "model" | "thinkingLevel" | "setActiveModelProfile" | "getActiveModelProfile"
83
+ >;
84
+ settings: Pick<Settings, "clearOverride" | "get" | "override" | "set">;
85
+ assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>;
86
+ }
87
+
88
+ function isReadonlyAssignmentMap(
89
+ assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>,
90
+ ): assignments is ReadonlyMap<GjcModelAssignmentTargetId, string> {
91
+ return typeof (assignments as { entries?: unknown }).entries === "function";
92
+ }
93
+
94
+ function getMaterializedAssignments(
95
+ assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>,
96
+ ): Array<[GjcModelAssignmentTargetId, string]> {
97
+ if (isReadonlyAssignmentMap(assignments)) return [...assignments.entries()];
98
+ const assignmentRecord: Partial<Record<GjcModelAssignmentTargetId, string>> = assignments;
99
+ const result: Array<[GjcModelAssignmentTargetId, string]> = [];
100
+ for (const role of Object.keys(assignmentRecord) as GjcModelAssignmentTargetId[]) {
101
+ const selector = assignmentRecord[role];
102
+ if (selector !== undefined) result.push([role, selector]);
103
+ }
104
+ return result;
105
+ }
106
+
79
107
  export function materializeActiveModelProfileAssignment(options: MaterializeModelProfileAssignmentOptions): boolean {
80
108
  const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
81
109
  if (!activeProfile) return false;
@@ -109,6 +137,43 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
109
137
  return true;
110
138
  }
111
139
 
140
+ export function materializeActiveModelProfileAssignments(options: MaterializeModelProfileAssignmentsOptions): boolean {
141
+ const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
142
+ if (!activeProfile) return false;
143
+
144
+ const materializedAssignments = getMaterializedAssignments(options.assignments);
145
+ if (materializedAssignments.length === 0) return true;
146
+
147
+ const nextModelRoles = { ...options.settings.get("modelRoles") };
148
+ const nextAgentModelOverrides = { ...options.settings.get("task.agentModelOverrides") };
149
+ const includesDefault = materializedAssignments.some(([role]) => role === "default");
150
+
151
+ if (!includesDefault && !nextModelRoles.default && options.session.model) {
152
+ nextModelRoles.default = formatModelSelectorValue(
153
+ `${options.session.model.provider}/${options.session.model.id}`,
154
+ options.session.thinkingLevel,
155
+ );
156
+ }
157
+
158
+ for (const [role, selector] of materializedAssignments) {
159
+ const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
160
+ if (target.settingsPath === "modelRoles") {
161
+ nextModelRoles[role] = selector;
162
+ } else {
163
+ nextAgentModelOverrides[role] = selector;
164
+ }
165
+ }
166
+
167
+ options.settings.set("modelRoles", nextModelRoles);
168
+ options.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
169
+ options.settings.set("modelProfile.default", undefined);
170
+ options.settings.clearOverride("modelProfile.default");
171
+ options.settings.override("modelRoles", nextModelRoles);
172
+ options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
173
+ options.session.setActiveModelProfile?.(undefined);
174
+ return true;
175
+ }
176
+
112
177
  export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
113
178
  return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
114
179
  }
@@ -923,10 +923,22 @@ function resolveCustomModelReference(modelId: string): Model<Api> | undefined {
923
923
  }
924
924
 
925
925
  function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
926
- if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
926
+ if (model.contextWindow !== undefined) {
927
927
  return model;
928
928
  }
929
- return { ...model, contextWindow: 1_000_000 };
929
+ if (model.id === "gpt-5.4" && model.provider !== "github-copilot") {
930
+ return { ...model, contextWindow: 1_000_000 };
931
+ }
932
+ // Custom GPT-5.5 endpoints that are not first-party `openai-responses` are
933
+ // typically Codex passthrough proxies (e.g. CLIProxyAPI fronting
934
+ // chatgpt.com/backend-api/codex), where the request path enforces the 272K
935
+ // prompt budget even though the model advertises a 1M total window.
936
+ // Without this default the bundled reference resolves to 1M, compaction
937
+ // never fires in time, and requests die with context_length_exceeded.
938
+ if (model.id === "gpt-5.5" && model.api !== "openai-responses") {
939
+ return { ...model, contextWindow: 272_000 };
940
+ }
941
+ return model;
930
942
  }
931
943
 
932
944
  function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
@@ -545,6 +545,42 @@ export const SETTINGS_SCHEMA = {
545
545
  ],
546
546
  },
547
547
  },
548
+
549
+ "tools.readArtifactSpillThreshold": {
550
+ type: "number",
551
+ default: 0,
552
+ ui: {
553
+ tab: "tools",
554
+ label: "Experimental read-tool artifact spill threshold (KB)",
555
+ description:
556
+ "Advanced opt-in only: combined-size cap for `read` output across all requested ranges. Above this the full output is saved as an artifact and a bounded head+tail snippet is kept inline. Live layofflabs/gpt-5.5 medium evidence on 2026-07-07 found no token savings and a lower cache-hit rate at 50 KB, so normal coding sessions should leave this Off unless intentionally measuring large-read behavior.",
557
+ options: [
558
+ { value: "0", label: "Off", description: "Default; no read-specific spill (backstop only)" },
559
+ { value: "50", label: "50 KB", description: "~12.5K tokens" },
560
+ { value: "100", label: "100 KB", description: "~25K tokens" },
561
+ { value: "256", label: "256 KB", description: "~64K tokens" },
562
+ { value: "512", label: "512 KB", description: "~128K tokens" },
563
+ { value: "1000", label: "1 MB", description: "~250K tokens" },
564
+ ],
565
+ },
566
+ },
567
+
568
+ "tools.fileMentionInlineBytes": {
569
+ type: "number",
570
+ default: 20,
571
+ ui: {
572
+ tab: "tools",
573
+ label: "File-mention inline cap (KB)",
574
+ description:
575
+ "Inline byte cap for auto-read `@path` file mentions, deliberately below the read-tool cap so an incidental mention injects a smaller snippet than an explicit read. The full file is still available via the read tool.",
576
+ options: [
577
+ { value: "5", label: "5 KB", description: "~1.25K tokens" },
578
+ { value: "10", label: "10 KB", description: "~2.5K tokens" },
579
+ { value: "20", label: "20 KB", description: "Default; ~5K tokens" },
580
+ { value: "50", label: "50 KB", description: "~12.5K tokens (matches read cap)" },
581
+ ],
582
+ },
583
+ },
548
584
  "tools.artifactTailBytes": {
549
585
  type: "number",
550
586
  default: 20,
@@ -623,6 +659,25 @@ export const SETTINGS_SCHEMA = {
623
659
  },
624
660
  },
625
661
 
662
+ "tools.maxInlineResultBytes": {
663
+ type: "number",
664
+ default: 0,
665
+ ui: {
666
+ tab: "tools",
667
+ label: "Max inline tool-result size (KB)",
668
+ description:
669
+ "Absolute backstop cap on inline tool-result text, enforced after artifact spill for every tool (including read and tools that set their own partial artifact meta). Output above this size is force-saved as an artifact and truncated to head+tail. 0 disables (default; opt-in pending measurement).",
670
+ options: [
671
+ { value: "0", label: "Off", description: "Disabled; no absolute inline cap" },
672
+ { value: "20", label: "20 KB", description: "~5K tokens" },
673
+ { value: "30", label: "30 KB", description: "~7.5K tokens" },
674
+ { value: "50", label: "50 KB", description: "~12.5K tokens" },
675
+ { value: "75", label: "75 KB", description: "~19K tokens" },
676
+ { value: "100", label: "100 KB", description: "~25K tokens" },
677
+ ],
678
+ },
679
+ },
680
+
626
681
  "statusLine.showHookStatus": {
627
682
  type: "boolean",
628
683
  default: false,
@@ -1385,6 +1440,15 @@ export const SETTINGS_SCHEMA = {
1385
1440
 
1386
1441
  "compaction.remoteEndpoint": { type: "string", default: undefined },
1387
1442
 
1443
+ // Below-threshold maintenance pruning (Finding 13). Internal/measurement-only:
1444
+ // keep the runtime setting for targeted experiments, but do not expose it in
1445
+ // settings UI. Live layofflabs/gpt-5.5 medium evidence on 2026-07-07 showed no
1446
+ // savings and a cache-hit regression; pruning rewrites already-sent history and
1447
+ // can force a provider cache-epoch reset.
1448
+ "compaction.maintenancePruningEnabled": { type: "boolean", default: false },
1449
+
1450
+ "compaction.maintenancePruningMinSavingsTokens": { type: "number", default: 8000 },
1451
+
1388
1452
  // Idle compaction
1389
1453
  "compaction.idleEnabled": {
1390
1454
  type: "boolean",
@@ -1658,7 +1722,8 @@ export const SETTINGS_SCHEMA = {
1658
1722
  ui: {
1659
1723
  tab: "context",
1660
1724
  label: "TTSR Context Mode",
1661
- description: "What to do with partial output when TTSR triggers",
1725
+ description:
1726
+ "What to do with partial output when TTSR triggers. 'discard' (recommended) drops the aborted partial turn so it never accumulates in context. 'keep' retains every aborted partial turn, which grows the token footprint each time a rule fires — prefer 'discard' unless you specifically need the partial output.",
1662
1727
  },
1663
1728
  },
1664
1729
 
@@ -3219,6 +3284,8 @@ export interface CompactionSettings {
3219
3284
  autoContinue: boolean;
3220
3285
  remoteEnabled: boolean;
3221
3286
  remoteEndpoint: string | undefined;
3287
+ maintenancePruningEnabled: boolean;
3288
+ maintenancePruningMinSavingsTokens: number;
3222
3289
  idleEnabled: boolean;
3223
3290
  idleThresholdTokens: number;
3224
3291
  idleTimeoutSeconds: number;
@@ -475,6 +475,28 @@ export class Settings {
475
475
  this.override("modelRoles", { ...shallowStringRecord(runtimeOverrides), [role]: modelId });
476
476
  }
477
477
  }
478
+ /**
479
+ * Set an agent model override while keeping any live runtime override aligned.
480
+ *
481
+ * Runtime model profiles override `task.agentModelOverrides` for the current
482
+ * session. A user-selected role assignment must win immediately in that same
483
+ * session, but only the explicit agent change should be persisted.
484
+ */
485
+ setAgentModelOverride(agentName: string, modelId: string): void {
486
+ const current = shallowStringRecord(getByPath(this.#global, ["task", "agentModelOverrides"]));
487
+ const runtimeOverrides = getByPath(this.#overrides, ["task", "agentModelOverrides"]);
488
+ const updateRuntimeOverride =
489
+ !!runtimeOverrides && typeof runtimeOverrides === "object" && !Array.isArray(runtimeOverrides);
490
+
491
+ this.set("task.agentModelOverrides", { ...current, [agentName]: modelId });
492
+
493
+ if (updateRuntimeOverride) {
494
+ this.override("task.agentModelOverrides", {
495
+ ...shallowStringRecord(runtimeOverrides),
496
+ [agentName]: modelId,
497
+ });
498
+ }
499
+ }
478
500
 
479
501
  /**
480
502
  * Get a model role (helper for modelRoles record).
@@ -175,6 +175,7 @@ type CoordinatorEventKind =
175
175
  | "turn.queued"
176
176
  | "turn.delivering"
177
177
  | "turn.active"
178
+ | "turn.acknowledged"
178
179
  | "turn.waiting_for_answer"
179
180
  | "turn.completed"
180
181
  | "turn.failed"
@@ -1121,6 +1122,18 @@ async function markTurnAcknowledgedFromRuntimeState(
1121
1122
  };
1122
1123
  await writeTurnRecord(namespaceDir, acknowledged);
1123
1124
  await writeActiveTurn(namespaceDir, acknowledged);
1125
+ await appendCoordinatorEvent(namespaceDir, {
1126
+ kind: "turn.acknowledged",
1127
+ sessionId: acknowledged.session_id,
1128
+ turnId: acknowledged.turn_id,
1129
+ summary: `Turn ${acknowledged.turn_id} was acknowledged by the GJC runtime`,
1130
+ payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, acknowledged.turn_id)),
1131
+ metadata: {
1132
+ status: acknowledged.status,
1133
+ tmux_keys_sent: acknowledged.delivery.tmux_keys_sent ?? null,
1134
+ prompt_acknowledged: true,
1135
+ },
1136
+ });
1124
1137
  return acknowledged;
1125
1138
  }
1126
1139
 
@@ -1533,14 +1546,27 @@ async function waitForCoordinatorEvents(namespaceDir: string, timeoutMs: number)
1533
1546
  };
1534
1547
  const timer = setTimeout(finish, Math.max(timeoutMs, 0));
1535
1548
  timer.unref?.();
1536
- await ensureDir(eventsDir(namespaceDir));
1537
- try {
1538
- const watcher = nodeFs.watch(eventsDir(namespaceDir), (_eventType, filename) => {
1539
- if (filename === "event-journal.jsonl" || filename === "latest-seq.json") finish();
1540
- });
1541
- watchers.push(watcher);
1542
- } catch {
1543
- // Directory may not exist yet; the timeout remains a bounded fallback.
1549
+ const eventDir = eventsDir(namespaceDir);
1550
+ const watchedDirs = [
1551
+ eventDir,
1552
+ turnsDir(namespaceDir),
1553
+ path.join(namespaceDir, "active-turns"),
1554
+ path.join(namespaceDir, "session-states"),
1555
+ ];
1556
+ for (const dir of watchedDirs) {
1557
+ await ensureDir(dir);
1558
+ try {
1559
+ const watcher = nodeFs.watch(dir, (_eventType, filename) => {
1560
+ if (dir === eventDir) {
1561
+ if (filename === "event-journal.jsonl" || filename === "latest-seq.json") finish();
1562
+ return;
1563
+ }
1564
+ if (typeof filename === "string" && filename.endsWith(".json")) finish();
1565
+ });
1566
+ watchers.push(watcher);
1567
+ } catch {
1568
+ // Directory may not be watchable on this platform; the timeout remains a bounded fallback.
1569
+ }
1544
1570
  }
1545
1571
  return deferred.promise;
1546
1572
  }