@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.
- package/CHANGELOG.md +63 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +105 -1
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +17 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +28 -1
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +120 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config.d.ts +14 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +23 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/notifications/threaded-render.d.ts +6 -0
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +53 -1
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +7 -7
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +61 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +91 -7
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +29 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-registry.ts +14 -2
- package/src/config/settings-schema.ts +68 -1
- package/src/config/settings.ts +22 -0
- package/src/coordinator-mcp/server.ts +34 -8
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +50 -18
- package/src/defaults/gjc/skills/ralplan/SKILL.md +40 -99
- package/src/defaults/gjc/skills/team/SKILL.md +17 -14
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +183 -219
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
- package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
- package/src/gjc-runtime/deep-interview-state.ts +44 -2
- package/src/gjc-runtime/session-state-sidecar.ts +263 -20
- package/src/gjc-runtime/state-runtime.ts +12 -2
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +187 -15
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1663 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +124 -4
- package/src/internal-urls/docs-index.generated.ts +16 -7
- package/src/main.ts +95 -43
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +160 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +66 -47
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +180 -18
- package/src/modes/controllers/selector-controller.ts +122 -27
- package/src/modes/interactive-mode.ts +51 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/rpc/rpc-mode.ts +29 -3
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/notifications/config.ts +25 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +117 -14
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-cli.ts +2 -2
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +302 -51
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/notifications/threaded-render.ts +17 -1
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +29 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/computer.md +2 -2
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +1 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +52 -28
- package/src/session/agent-session.ts +951 -132
- package/src/session/agent-storage.ts +4 -0
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +103 -4
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +218 -71
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +132 -24
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +13 -6
- package/src/task/output-manager.ts +20 -8
- package/src/task/receipt.ts +26 -7
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/computer.ts +246 -43
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/tools/telegram-send.ts +6 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/web/scrapers/youtube.ts +24 -12
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
package/src/config/file-lock.ts
CHANGED
|
@@ -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<
|
|
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
|
|
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
|
|
60
|
-
* token as close to the unlink as possible
|
|
61
|
-
* STILL holds the exact `{pid, timestamp}`
|
|
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
|
|
64
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
119
|
-
if (liveness === "
|
|
120
|
-
|
|
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
|
|
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.
|
|
126
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
197
|
+
const owner = await tryAcquireLock(lockPath);
|
|
198
|
+
if (owner) {
|
|
199
|
+
return () => releaseLock(lockPath, owner);
|
|
161
200
|
}
|
|
162
201
|
|
|
163
|
-
|
|
164
|
-
|
|
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: "
|
|
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.
|
|
926
|
+
if (model.contextWindow !== undefined) {
|
|
927
927
|
return model;
|
|
928
928
|
}
|
|
929
|
-
|
|
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:
|
|
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;
|
package/src/config/settings.ts
CHANGED
|
@@ -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
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
}
|