@gajae-code/coding-agent 0.11.3 → 0.11.5
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 +47 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
- package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/owner.d.ts +3 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -0
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
- package/dist/types/plan-mode/approved-plan.d.ts +1 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
- package/dist/types/sdk/bus/config-commands.d.ts +8 -0
- package/dist/types/sdk/bus/config.d.ts +22 -0
- package/dist/types/sdk/bus/index.d.ts +162 -2
- package/dist/types/sdk/bus/notification-service.d.ts +71 -8
- package/dist/types/sdk/bus/session-control.d.ts +11 -8
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
- package/dist/types/sdk/session.d.ts +16 -1
- package/dist/types/secrets/index.d.ts +4 -2
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +31 -2
- package/dist/types/session/artifacts.d.ts +30 -8
- package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
- package/dist/types/session/session-manager.d.ts +88 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/tool-discovery/tool-index.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +87 -12
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +25 -9
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +10 -4
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +17 -0
- package/src/daemon/runtime.ts +2 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
- package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
- package/src/defaults/gjc/skills/team/SKILL.md +7 -9
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
- package/src/gjc-runtime/deep-interview-state.ts +127 -3
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/launch-tmux.ts +61 -6
- package/src/gjc-runtime/managed-owner-admission.ts +352 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +80 -222
- package/src/gjc-runtime/team-runtime.ts +1 -1
- package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
- package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
- package/src/harness-control-plane/owner.ts +59 -3
- package/src/hindsight/client.ts +65 -2
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +37 -13
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -37
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +393 -0
- package/src/modes/components/transcript-viewer-overlay.ts +63 -1
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/plan-mode-controller.ts +2 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +71 -9
- package/src/modes/interactive-mode.ts +14 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/lsp.md +3 -3
- package/src/runtime-mcp/manager.ts +16 -1
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +309 -38
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +526 -130
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/config-commands.ts +22 -0
- package/src/sdk/bus/config.ts +37 -0
- package/src/sdk/bus/index.ts +384 -92
- package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
- package/src/sdk/bus/notification-service.ts +284 -62
- package/src/sdk/bus/recent-activity.ts +14 -5
- package/src/sdk/bus/session-control.ts +150 -44
- package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
- package/src/sdk/bus/telegram-daemon-control.ts +294 -76
- package/src/sdk/bus/telegram-daemon.ts +1842 -281
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +155 -27
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +495 -253
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +215 -39
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1055 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/task/executor.ts +71 -26
- package/src/task/index.ts +63 -20
- package/src/tool-discovery/tool-index.ts +18 -0
- package/src/tools/ask.ts +329 -57
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/read.ts +7 -1
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { withFileLock } from "../../config/file-lock";
|
|
@@ -48,6 +48,27 @@ export interface SessionList {
|
|
|
48
48
|
sessions: IndexedSession[];
|
|
49
49
|
warnings: string[];
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
export interface SessionIndexDiagnosis {
|
|
53
|
+
status: "healthy" | "corrupt" | "unsupported";
|
|
54
|
+
validPrefixSeq: number;
|
|
55
|
+
snapshotSeq: number;
|
|
56
|
+
reason?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SessionIndexRepairResult extends SessionIndexDiagnosis {
|
|
60
|
+
repaired: boolean;
|
|
61
|
+
quarantinePath?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface SessionIndexScan {
|
|
65
|
+
diagnosis: SessionIndexDiagnosis;
|
|
66
|
+
snapshotEvents: SessionIndexEvent[];
|
|
67
|
+
validLogEvents: SessionIndexEvent[];
|
|
68
|
+
snapshotContents: Buffer | undefined;
|
|
69
|
+
logContents: Buffer | undefined;
|
|
70
|
+
unsupportedError?: UnsupportedStateVersionError;
|
|
71
|
+
}
|
|
51
72
|
const canonical = (event: Omit<SessionIndexEvent, "checksum">) => JSON.stringify(event);
|
|
52
73
|
export const sessionIndexChecksum = (event: Omit<SessionIndexEvent, "checksum">) =>
|
|
53
74
|
createHash("sha256").update(canonical(event)).digest("hex");
|
|
@@ -142,6 +163,27 @@ async function syncDirectory(file: string): Promise<void> {
|
|
|
142
163
|
}
|
|
143
164
|
}
|
|
144
165
|
|
|
166
|
+
async function writeAndSync(file: string, contents: Buffer | string): Promise<void> {
|
|
167
|
+
const handle = await fs.open(file, "w", 0o600);
|
|
168
|
+
try {
|
|
169
|
+
await handle.writeFile(contents);
|
|
170
|
+
await handle.sync();
|
|
171
|
+
} finally {
|
|
172
|
+
await handle.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function replaceAtomically(file: string, contents: Buffer | string): Promise<void> {
|
|
177
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
178
|
+
try {
|
|
179
|
+
await writeAndSync(temporary, contents);
|
|
180
|
+
await fs.rename(temporary, file);
|
|
181
|
+
await syncDirectory(file);
|
|
182
|
+
} finally {
|
|
183
|
+
await fs.rm(temporary, { force: true });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
145
187
|
function alive(pid: number): boolean {
|
|
146
188
|
try {
|
|
147
189
|
process.kill(pid, 0);
|
|
@@ -150,7 +192,14 @@ function alive(pid: number): boolean {
|
|
|
150
192
|
return (e as NodeJS.ErrnoException).code === "EPERM";
|
|
151
193
|
}
|
|
152
194
|
}
|
|
195
|
+
|
|
196
|
+
interface SessionIndexOpenGroup {
|
|
197
|
+
promise: Promise<void>;
|
|
198
|
+
closed: boolean;
|
|
199
|
+
}
|
|
153
200
|
export class SessionIndex {
|
|
201
|
+
static #operations = new Map<string, Promise<void>>();
|
|
202
|
+
static #openGroups = new Map<string, SessionIndexOpenGroup>();
|
|
154
203
|
#agentDir: string;
|
|
155
204
|
#events: SessionIndexEvent[] = [];
|
|
156
205
|
#warnings: string[] = [];
|
|
@@ -159,42 +208,249 @@ export class SessionIndex {
|
|
|
159
208
|
constructor(agentDir: string) {
|
|
160
209
|
this.#agentDir = agentDir;
|
|
161
210
|
}
|
|
211
|
+
static #enqueue<T>(indexPath: string, operation: () => Promise<T>): Promise<T> {
|
|
212
|
+
const previous = SessionIndex.#operations.get(indexPath) ?? Promise.resolve();
|
|
213
|
+
const promise = previous.catch(() => {}).then(operation);
|
|
214
|
+
const completion = promise.then(
|
|
215
|
+
() => {},
|
|
216
|
+
() => {},
|
|
217
|
+
);
|
|
218
|
+
SessionIndex.#operations.set(indexPath, completion);
|
|
219
|
+
void completion.then(() => {
|
|
220
|
+
if (SessionIndex.#operations.get(indexPath) === completion) SessionIndex.#operations.delete(indexPath);
|
|
221
|
+
});
|
|
222
|
+
return promise;
|
|
223
|
+
}
|
|
162
224
|
async open(): Promise<this> {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
225
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
226
|
+
let group = SessionIndex.#openGroups.get(indexPath);
|
|
227
|
+
if (!group || group.closed) {
|
|
228
|
+
group = { promise: Promise.resolve(), closed: false };
|
|
229
|
+
SessionIndex.#openGroups.set(indexPath, group);
|
|
230
|
+
group.promise = SessionIndex.#enqueue(indexPath, () => this.#prepareOpenGroup(indexPath, group!));
|
|
231
|
+
}
|
|
232
|
+
await group.promise;
|
|
233
|
+
await SessionIndex.#enqueue(indexPath, () => withFileLock(logFor(this.#agentDir), () => this.#replayUnderLock()));
|
|
166
234
|
return this;
|
|
167
235
|
}
|
|
236
|
+
async #prepareOpenGroup(indexPath: string, group: SessionIndexOpenGroup): Promise<void> {
|
|
237
|
+
try {
|
|
238
|
+
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
239
|
+
await fs.chmod(dirFor(this.#agentDir), 0o700);
|
|
240
|
+
} finally {
|
|
241
|
+
group.closed = true;
|
|
242
|
+
if (SessionIndex.#openGroups.get(indexPath) === group) SessionIndex.#openGroups.delete(indexPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
168
245
|
async replay(): Promise<void> {
|
|
169
|
-
|
|
246
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
247
|
+
await SessionIndex.#enqueue(indexPath, () => withFileLock(logFor(this.#agentDir), () => this.#replayUnderLock()));
|
|
248
|
+
}
|
|
249
|
+
async #replayUnderLock(): Promise<void> {
|
|
250
|
+
const scan = await this.#scan();
|
|
251
|
+
if (scan.diagnosis.status === "unsupported") throw scan.unsupportedError!;
|
|
252
|
+
this.#events = [...scan.snapshotEvents, ...scan.validLogEvents];
|
|
170
253
|
this.#warnings = [];
|
|
171
|
-
this.#logOffset = 0;
|
|
172
|
-
this.#corruptSuffix =
|
|
254
|
+
this.#logOffset = scan.logContents?.length ?? 0;
|
|
255
|
+
this.#corruptSuffix = scan.diagnosis.status === "corrupt";
|
|
256
|
+
if (scan.diagnosis.reason === "invalid snapshot") this.#warnings.push("Invalid session index snapshot");
|
|
257
|
+
if (this.#corruptSuffix) this.#warnings.push("Corrupt session index entry; replay truncated");
|
|
258
|
+
}
|
|
259
|
+
async #scan(): Promise<SessionIndexScan> {
|
|
260
|
+
let snapshotContents: Buffer | undefined;
|
|
261
|
+
let logContents: Buffer | undefined;
|
|
262
|
+
let snapshotEvents: SessionIndexEvent[] = [];
|
|
173
263
|
let snapshotSeq = 0;
|
|
264
|
+
let trustedSnapshotSeq = 0;
|
|
265
|
+
let invalidSnapshot = false;
|
|
266
|
+
let unsupportedError: UnsupportedStateVersionError | undefined;
|
|
174
267
|
try {
|
|
175
|
-
|
|
268
|
+
snapshotContents = await fs.readFile(snapshotFor(this.#agentDir));
|
|
269
|
+
const snapshot = JSON.parse(snapshotContents.toString("utf8")) as {
|
|
176
270
|
version?: number;
|
|
177
|
-
|
|
178
|
-
|
|
271
|
+
indexSeq?: unknown;
|
|
272
|
+
events?: unknown;
|
|
179
273
|
};
|
|
274
|
+
if (typeof snapshot.indexSeq === "number" && Number.isSafeInteger(snapshot.indexSeq) && snapshot.indexSeq >= 0)
|
|
275
|
+
snapshotSeq = snapshot.indexSeq;
|
|
180
276
|
assertSupportedSnapshotVersion(snapshotFor(this.#agentDir), snapshot);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
277
|
+
const supportedEvents: SessionIndexEvent[] = [];
|
|
278
|
+
if (Array.isArray(snapshot.events)) {
|
|
279
|
+
try {
|
|
280
|
+
for (const event of snapshot.events) {
|
|
281
|
+
assertSupportedStateVersion(snapshotFor(this.#agentDir), event);
|
|
282
|
+
supportedEvents.push(event as SessionIndexEvent);
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (!(error instanceof UnsupportedStateVersionError)) throw error;
|
|
286
|
+
unsupportedError = error;
|
|
287
|
+
snapshotEvents = supportedEvents;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!unsupportedError) {
|
|
291
|
+
if (!isValidSnapshot(snapshot)) invalidSnapshot = true;
|
|
292
|
+
else {
|
|
293
|
+
snapshotEvents = snapshot.events;
|
|
294
|
+
trustedSnapshotSeq = snapshot.indexSeq;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (error instanceof UnsupportedStateVersionError) unsupportedError = error;
|
|
299
|
+
else if ((error as NodeJS.ErrnoException).code !== "ENOENT") invalidSnapshot = true;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
logContents = await fs.readFile(logFor(this.#agentDir));
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
305
|
+
}
|
|
306
|
+
const validLogEvents: SessionIndexEvent[] = [];
|
|
307
|
+
let corrupt = invalidSnapshot;
|
|
308
|
+
let logCorrupt = false;
|
|
309
|
+
let tailStarted = false;
|
|
310
|
+
let historicalLast: number | undefined;
|
|
311
|
+
let expected = trustedSnapshotSeq + 1;
|
|
312
|
+
if (logContents) {
|
|
313
|
+
const text = logContents.toString("utf8");
|
|
314
|
+
const lines = text.split("\n");
|
|
315
|
+
const terminal = lines.pop();
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
if (!line) continue;
|
|
318
|
+
try {
|
|
319
|
+
const event = JSON.parse(line) as SessionIndexEvent;
|
|
320
|
+
assertSupportedStateVersion(logFor(this.#agentDir), event);
|
|
321
|
+
const { checksum, ...unsigned } = event;
|
|
322
|
+
if (checksum !== sessionIndexChecksum(unsigned)) {
|
|
323
|
+
corrupt = true;
|
|
324
|
+
logCorrupt = true;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (!tailStarted && !invalidSnapshot && event.indexSeq <= trustedSnapshotSeq) {
|
|
328
|
+
if (
|
|
329
|
+
!Number.isSafeInteger(event.indexSeq) ||
|
|
330
|
+
event.indexSeq <= 0 ||
|
|
331
|
+
(historicalLast !== undefined && event.indexSeq !== historicalLast + 1)
|
|
332
|
+
) {
|
|
333
|
+
corrupt = true;
|
|
334
|
+
logCorrupt = true;
|
|
335
|
+
} else {
|
|
336
|
+
historicalLast = event.indexSeq;
|
|
337
|
+
}
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
tailStarted = true;
|
|
341
|
+
if (historicalLast !== undefined && historicalLast !== trustedSnapshotSeq) {
|
|
342
|
+
corrupt = true;
|
|
343
|
+
logCorrupt = true;
|
|
344
|
+
}
|
|
345
|
+
if (event.indexSeq !== expected) {
|
|
346
|
+
corrupt = true;
|
|
347
|
+
logCorrupt = true;
|
|
348
|
+
} else if (!logCorrupt) {
|
|
349
|
+
validLogEvents.push(event);
|
|
350
|
+
expected++;
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error instanceof UnsupportedStateVersionError) {
|
|
354
|
+
const verifiedSnapshotPrefix = snapshotEvents.at(-1)?.indexSeq ?? trustedSnapshotSeq;
|
|
355
|
+
const validPrefixSeq = validLogEvents.at(-1)?.indexSeq ?? verifiedSnapshotPrefix;
|
|
356
|
+
return {
|
|
357
|
+
diagnosis: { status: "unsupported", validPrefixSeq, snapshotSeq, reason: error.message },
|
|
358
|
+
snapshotEvents,
|
|
359
|
+
validLogEvents,
|
|
360
|
+
snapshotContents,
|
|
361
|
+
logContents,
|
|
362
|
+
unsupportedError: error,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
corrupt = true;
|
|
366
|
+
logCorrupt = true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (historicalLast !== undefined && !tailStarted && historicalLast !== trustedSnapshotSeq) {
|
|
370
|
+
corrupt = true;
|
|
371
|
+
logCorrupt = true;
|
|
372
|
+
}
|
|
373
|
+
if (terminal !== "") {
|
|
374
|
+
corrupt = true;
|
|
375
|
+
logCorrupt = true;
|
|
376
|
+
}
|
|
187
377
|
}
|
|
188
|
-
|
|
378
|
+
const verifiedSnapshotPrefix = snapshotEvents.at(-1)?.indexSeq ?? trustedSnapshotSeq;
|
|
379
|
+
const validPrefixSeq = validLogEvents.at(-1)?.indexSeq ?? verifiedSnapshotPrefix;
|
|
380
|
+
return {
|
|
381
|
+
diagnosis: {
|
|
382
|
+
status: unsupportedError ? "unsupported" : corrupt ? "corrupt" : "healthy",
|
|
383
|
+
validPrefixSeq,
|
|
384
|
+
snapshotSeq,
|
|
385
|
+
reason:
|
|
386
|
+
unsupportedError?.message ??
|
|
387
|
+
(invalidSnapshot ? "invalid snapshot" : corrupt ? "invalid log sequence" : undefined),
|
|
388
|
+
},
|
|
389
|
+
snapshotEvents,
|
|
390
|
+
validLogEvents,
|
|
391
|
+
snapshotContents,
|
|
392
|
+
logContents,
|
|
393
|
+
unsupportedError,
|
|
394
|
+
};
|
|
189
395
|
}
|
|
190
|
-
async
|
|
396
|
+
async diagnose(): Promise<SessionIndexDiagnosis> {
|
|
397
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
398
|
+
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
399
|
+
const exists = await Promise.all(
|
|
400
|
+
[snapshotFor(this.#agentDir), logFor(this.#agentDir)].map(async file => {
|
|
401
|
+
try {
|
|
402
|
+
await fs.stat(file);
|
|
403
|
+
return true;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
410
|
+
if (!exists.some(Boolean)) return { status: "healthy", validPrefixSeq: 0, snapshotSeq: 0 };
|
|
411
|
+
return await withFileLock(logFor(this.#agentDir), async () => (await this.#scan()).diagnosis);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
async repair(): Promise<SessionIndexRepairResult> {
|
|
415
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
416
|
+
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
417
|
+
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
418
|
+
return await withFileLock(logFor(this.#agentDir), async () => {
|
|
419
|
+
const scan = await this.#scan();
|
|
420
|
+
if (scan.diagnosis.status === "unsupported") return { ...scan.diagnosis, repaired: false };
|
|
421
|
+
if (scan.diagnosis.status === "healthy") return { ...scan.diagnosis, repaired: false };
|
|
422
|
+
const quarantineBase = path.join(dirFor(this.#agentDir), "quarantine");
|
|
423
|
+
await fs.mkdir(quarantineBase, { recursive: true, mode: 0o700 });
|
|
424
|
+
await syncDirectory(quarantineBase);
|
|
425
|
+
const quarantinePath = path.join(quarantineBase, `repair-${Date.now()}-${process.pid}-${randomUUID()}`);
|
|
426
|
+
await fs.mkdir(quarantinePath, { mode: 0o700 });
|
|
427
|
+
await syncDirectory(quarantinePath);
|
|
428
|
+
if (scan.snapshotContents)
|
|
429
|
+
await writeAndSync(path.join(quarantinePath, "index.snapshot.json"), scan.snapshotContents);
|
|
430
|
+
if (scan.logContents) await writeAndSync(path.join(quarantinePath, "index.jsonl"), scan.logContents);
|
|
431
|
+
await syncDirectory(path.join(quarantinePath, "index.jsonl"));
|
|
432
|
+
const events = [...scan.snapshotEvents, ...scan.validLogEvents];
|
|
433
|
+
const snapshot = JSON.stringify({
|
|
434
|
+
version: SESSION_INDEX_SNAPSHOT_VERSION,
|
|
435
|
+
indexSeq: scan.diagnosis.validPrefixSeq,
|
|
436
|
+
events,
|
|
437
|
+
});
|
|
438
|
+
const log = scan.validLogEvents.map(event => JSON.stringify(event)).join("\n");
|
|
439
|
+
await replaceAtomically(snapshotFor(this.#agentDir), snapshot);
|
|
440
|
+
await replaceAtomically(logFor(this.#agentDir), log ? `${log}\n` : "");
|
|
441
|
+
await this.#replayUnderLock();
|
|
442
|
+
return { ...scan.diagnosis, repaired: true, quarantinePath };
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
async #tailUnderLock(snapshotSeq = this.indexSeq, allowResync = true): Promise<void> {
|
|
191
447
|
let data: Buffer;
|
|
192
448
|
try {
|
|
193
449
|
const handle = await fs.open(logFor(this.#agentDir), "r");
|
|
194
450
|
try {
|
|
195
451
|
const stat = await handle.stat();
|
|
196
452
|
if (stat.size < this.#logOffset) {
|
|
197
|
-
if (allowResync) await this
|
|
453
|
+
if (allowResync) await this.#replayUnderLock();
|
|
198
454
|
else this.#warn("Session index log was truncated");
|
|
199
455
|
return;
|
|
200
456
|
}
|
|
@@ -232,7 +488,7 @@ export class SessionIndex {
|
|
|
232
488
|
if (corrupt) {
|
|
233
489
|
this.#corruptSuffix = true;
|
|
234
490
|
this.#warn("Corrupt session index entry; replay truncated");
|
|
235
|
-
if (allowResync) await this
|
|
491
|
+
if (allowResync) await this.#replayUnderLock();
|
|
236
492
|
}
|
|
237
493
|
}
|
|
238
494
|
#warn(message: string): void {
|
|
@@ -240,7 +496,13 @@ export class SessionIndex {
|
|
|
240
496
|
}
|
|
241
497
|
|
|
242
498
|
async refresh(): Promise<void> {
|
|
243
|
-
|
|
499
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
500
|
+
await SessionIndex.#enqueue(indexPath, () =>
|
|
501
|
+
withFileLock(logFor(this.#agentDir), () => this.#refreshUnderLock()),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
async #refreshUnderLock(): Promise<void> {
|
|
505
|
+
await this.#tailUnderLock();
|
|
244
506
|
}
|
|
245
507
|
get indexSeq(): number {
|
|
246
508
|
return this.#events.at(-1)?.indexSeq ?? 0;
|
|
@@ -249,28 +511,37 @@ export class SessionIndex {
|
|
|
249
511
|
input: Omit<SessionIndexEvent, "version" | "indexSeq" | "checksum" | "ts"> &
|
|
250
512
|
Partial<Pick<SessionIndexEvent, "ts">>,
|
|
251
513
|
): Promise<SessionIndexEvent> {
|
|
252
|
-
|
|
253
|
-
return
|
|
254
|
-
await
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
514
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
515
|
+
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
516
|
+
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
517
|
+
return await withFileLock(logFor(this.#agentDir), async () => {
|
|
518
|
+
await this.#replayUnderLock();
|
|
519
|
+
if (this.#corruptSuffix)
|
|
520
|
+
throw new Error(
|
|
521
|
+
"Cannot append to corrupt session index log; run `gjc gc --repair-session-index` to quarantine evidence and retain the valid prefix",
|
|
522
|
+
);
|
|
523
|
+
const unsigned: Omit<SessionIndexEvent, "checksum"> = {
|
|
524
|
+
...input,
|
|
525
|
+
version: SDK_STATE_VERSION,
|
|
526
|
+
indexSeq: this.indexSeq + 1,
|
|
527
|
+
ts: input.ts ?? Date.now(),
|
|
528
|
+
};
|
|
529
|
+
const event: SessionIndexEvent = { ...unsigned, checksum: sessionIndexChecksum(unsigned) };
|
|
530
|
+
await appendSync(logFor(this.#agentDir), JSON.stringify(event));
|
|
531
|
+
await this.#refreshUnderLock();
|
|
532
|
+
if ((await fs.stat(logFor(this.#agentDir))).size >= ROTATE_BYTES) await this.#rotate();
|
|
533
|
+
return event;
|
|
534
|
+
});
|
|
267
535
|
});
|
|
268
536
|
}
|
|
269
537
|
async snapshot(): Promise<void> {
|
|
270
|
-
|
|
538
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
539
|
+
await SessionIndex.#enqueue(indexPath, () =>
|
|
540
|
+
withFileLock(logFor(this.#agentDir), () => this.#snapshotUnderLock()),
|
|
541
|
+
);
|
|
271
542
|
}
|
|
272
543
|
async #snapshotUnderLock(): Promise<void> {
|
|
273
|
-
await this
|
|
544
|
+
await this.#replayUnderLock();
|
|
274
545
|
const file = snapshotFor(this.#agentDir);
|
|
275
546
|
let current: unknown;
|
|
276
547
|
try {
|
|
@@ -13,8 +13,11 @@ export class UnsupportedStateVersionError extends Error {
|
|
|
13
13
|
constructor(
|
|
14
14
|
readonly file: string,
|
|
15
15
|
readonly version: number,
|
|
16
|
+
readonly maximumSupportedVersion = SDK_STATE_VERSION,
|
|
16
17
|
) {
|
|
17
|
-
super(
|
|
18
|
+
super(
|
|
19
|
+
`Unsupported SDK state version ${version} in ${file}; maximum supported version is ${maximumSupportedVersion}.`,
|
|
20
|
+
);
|
|
18
21
|
this.name = "UnsupportedStateVersionError";
|
|
19
22
|
}
|
|
20
23
|
}
|
|
@@ -38,7 +41,7 @@ export function assertSupportedSnapshotVersion(file: string, value: unknown): vo
|
|
|
38
41
|
const record = value as { version?: unknown; stateVersion?: unknown };
|
|
39
42
|
for (const version of [record.version, record.stateVersion]) {
|
|
40
43
|
if (typeof version === "number" && Number.isFinite(version) && version > SESSION_INDEX_SNAPSHOT_VERSION) {
|
|
41
|
-
throw new UnsupportedStateVersionError(file, version);
|
|
44
|
+
throw new UnsupportedStateVersionError(file, version, SESSION_INDEX_SNAPSHOT_VERSION);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
}
|
|
@@ -26,11 +26,13 @@ export interface ChatDaemonRuntimeHandle {
|
|
|
26
26
|
export interface RunChatDaemonInternalDeps {
|
|
27
27
|
processPid?: number;
|
|
28
28
|
pidAlive?: (pid: number) => boolean;
|
|
29
|
+
pidIncarnation?: (pid: number) => string | undefined;
|
|
29
30
|
createRuntime?: (input: {
|
|
30
31
|
kind: ChatDaemonKind;
|
|
31
32
|
agentDir: string;
|
|
32
33
|
config: ChatDaemonConfig;
|
|
33
34
|
}) => Promise<ChatDaemonRuntimeHandle> | ChatDaemonRuntimeHandle;
|
|
35
|
+
renewHeartbeat?: (input: Parameters<typeof renewChatDaemonHeartbeat>[0]) => Promise<boolean>;
|
|
34
36
|
setInterval?: typeof setInterval;
|
|
35
37
|
clearInterval?: typeof clearInterval;
|
|
36
38
|
}
|
|
@@ -65,8 +67,10 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
|
|
|
65
67
|
idleTimeoutMs: 60_000,
|
|
66
68
|
rich: { enabled: true },
|
|
67
69
|
richDraft: { enabled: false },
|
|
70
|
+
toolActivity: { enabled: true },
|
|
68
71
|
topics: { nameTemplate: undefined },
|
|
69
72
|
btw: { enabled: true },
|
|
73
|
+
streaming: { enabled: true },
|
|
70
74
|
})
|
|
71
75
|
) {
|
|
72
76
|
throw new Error("Discord notifications are enabled but configuration is incomplete");
|
|
@@ -98,8 +102,10 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
|
|
|
98
102
|
idleTimeoutMs: 60_000,
|
|
99
103
|
rich: { enabled: true },
|
|
100
104
|
richDraft: { enabled: false },
|
|
105
|
+
toolActivity: { enabled: true },
|
|
101
106
|
topics: { nameTemplate: undefined },
|
|
102
107
|
btw: { enabled: true },
|
|
108
|
+
streaming: { enabled: true },
|
|
103
109
|
})
|
|
104
110
|
) {
|
|
105
111
|
throw new Error("Slack notifications are enabled but configuration is incomplete");
|
|
@@ -160,10 +166,19 @@ export async function runChatDaemonInternal(
|
|
|
160
166
|
if (!ownerId) throw new Error("missing --owner-id");
|
|
161
167
|
const pid = ownerPid(ownerId);
|
|
162
168
|
if (pid !== undefined && !(deps.pidAlive ?? defaultPidAlive)(pid)) return;
|
|
169
|
+
const daemonPid = deps.processPid ?? process.pid;
|
|
163
170
|
const config = await loadConfig(agentDir, kind);
|
|
164
171
|
if (!config) return;
|
|
165
172
|
if (
|
|
166
|
-
!(await acquireChatDaemonOwnership({
|
|
173
|
+
!(await acquireChatDaemonOwnership({
|
|
174
|
+
agentDir,
|
|
175
|
+
kind,
|
|
176
|
+
ownerId,
|
|
177
|
+
pid: daemonPid,
|
|
178
|
+
identity: config.identity,
|
|
179
|
+
pidAlive: deps.pidAlive,
|
|
180
|
+
pidIncarnation: deps.pidIncarnation,
|
|
181
|
+
}))
|
|
167
182
|
)
|
|
168
183
|
return;
|
|
169
184
|
|
|
@@ -171,30 +186,60 @@ export async function runChatDaemonInternal(
|
|
|
171
186
|
let runtime: ChatDaemonRuntimeHandle | undefined;
|
|
172
187
|
let interval: ReturnType<typeof setInterval> | undefined;
|
|
173
188
|
let stopping = false;
|
|
189
|
+
let terminalError: unknown;
|
|
190
|
+
let runtimeStop: Promise<void> | undefined;
|
|
191
|
+
const stopRuntime = (): Promise<void> => {
|
|
192
|
+
runtimeStop ??= runtime?.stop() ?? Promise.resolve();
|
|
193
|
+
return runtimeStop;
|
|
194
|
+
};
|
|
174
195
|
const stop = (): void => {
|
|
175
196
|
stopping = true;
|
|
197
|
+
void stopRuntime().catch(error => {
|
|
198
|
+
terminalError ??= error;
|
|
199
|
+
});
|
|
176
200
|
};
|
|
177
201
|
try {
|
|
178
202
|
incarnation = (await readChatDaemonState(agentDir, kind))?.incarnation;
|
|
179
203
|
if (!incarnation) throw new Error("chat daemon ownership state is missing an incarnation");
|
|
180
204
|
runtime = await (deps.createRuntime?.({ kind, agentDir, config }) ?? defaultRuntime({ kind, agentDir, config }));
|
|
181
|
-
const
|
|
182
|
-
|
|
205
|
+
const activeRuntime = runtime;
|
|
206
|
+
const renewHeartbeat = async (): Promise<boolean> =>
|
|
207
|
+
await (deps.renewHeartbeat ?? renewChatDaemonHeartbeat)({
|
|
183
208
|
agentDir,
|
|
184
209
|
kind,
|
|
185
210
|
ownerId,
|
|
186
|
-
pid:
|
|
211
|
+
pid: daemonPid,
|
|
187
212
|
incarnation,
|
|
188
|
-
transportHealthy:
|
|
213
|
+
transportHealthy: activeRuntime.transportHealthy?.() ?? true,
|
|
214
|
+
pidAlive: deps.pidAlive,
|
|
215
|
+
pidIncarnation: deps.pidIncarnation,
|
|
189
216
|
});
|
|
217
|
+
const terminateForLostOwnership = async (): Promise<void> => {
|
|
218
|
+
stopping = true;
|
|
219
|
+
await stopRuntime();
|
|
190
220
|
};
|
|
191
221
|
process.once("SIGTERM", stop);
|
|
192
222
|
process.once("SIGINT", stop);
|
|
223
|
+
if (!(await renewHeartbeat())) {
|
|
224
|
+
await terminateForLostOwnership();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await runtime.start();
|
|
193
228
|
interval = (deps.setInterval ?? setInterval)(() => {
|
|
194
|
-
void
|
|
229
|
+
void (async () => {
|
|
230
|
+
try {
|
|
231
|
+
if (!(await renewHeartbeat())) await terminateForLostOwnership();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
terminalError ??= error;
|
|
234
|
+
stopping = true;
|
|
235
|
+
try {
|
|
236
|
+
await stopRuntime();
|
|
237
|
+
} catch (stopError) {
|
|
238
|
+
terminalError ??= stopError;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
})();
|
|
195
242
|
}, 5_000);
|
|
196
|
-
await runtime.start();
|
|
197
|
-
await renewHeartbeat();
|
|
198
243
|
while (!stopping) {
|
|
199
244
|
const request = await readChatDaemonControlRequest(agentDir, kind);
|
|
200
245
|
if (request?.ownerId === ownerId && request.incarnation === incarnation) {
|
|
@@ -208,9 +253,26 @@ export async function runChatDaemonInternal(
|
|
|
208
253
|
process.off("SIGTERM", stop);
|
|
209
254
|
process.off("SIGINT", stop);
|
|
210
255
|
try {
|
|
211
|
-
await
|
|
256
|
+
await stopRuntime();
|
|
257
|
+
} catch (error) {
|
|
258
|
+
terminalError ??= error;
|
|
212
259
|
} finally {
|
|
213
|
-
|
|
260
|
+
if (incarnation !== undefined) {
|
|
261
|
+
try {
|
|
262
|
+
await releaseChatDaemonOwnership({
|
|
263
|
+
agentDir,
|
|
264
|
+
kind,
|
|
265
|
+
ownerId,
|
|
266
|
+
pid: daemonPid,
|
|
267
|
+
incarnation,
|
|
268
|
+
pidAlive: deps.pidAlive,
|
|
269
|
+
pidIncarnation: deps.pidIncarnation,
|
|
270
|
+
});
|
|
271
|
+
} catch (error) {
|
|
272
|
+
terminalError ??= error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
214
275
|
}
|
|
215
276
|
}
|
|
277
|
+
if (terminalError !== undefined) throw terminalError;
|
|
216
278
|
}
|