@gajae-code/coding-agent 0.11.7 → 0.11.9
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 +73 -1
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/keybindings.d.ts +33 -3
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/settings-schema.d.ts +15 -2
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +50 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commands/sdk.ts +2 -2
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +2 -3
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +842 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +78 -4
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/state-writer.ts +166 -2
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/internal-urls/docs-index.generated.ts +8 -6
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +20 -4
- package/src/modes/components/welcome.ts +32 -20
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +70 -36
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +7 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/json-rpc.ts +7 -4
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/runtime-mcp/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +20 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +27 -2
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +853 -112
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +138 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +998 -133
- package/src/session/internal/managed-session-storage.ts +268 -203
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +406 -360
- package/src/session/session-storage.ts +151 -11
- package/src/skill-state/active-state.ts +10 -0
- package/src/task/executor.ts +13 -5
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +199 -88
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const REDACTED = "<redacted>";
|
|
2
|
+
const SENSITIVE_KEY_PATTERN = /(?:token|secret|key|credential|password|authorization|auth|bearer|cookie|session)/i;
|
|
3
|
+
|
|
4
|
+
export function redactMCPEndpoint(value: string | undefined): string | undefined {
|
|
5
|
+
if (!value) return value;
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
if (url.username) url.username = REDACTED;
|
|
9
|
+
if (url.password) url.password = REDACTED;
|
|
10
|
+
if (url.pathname !== "/") url.pathname = `/${REDACTED}`;
|
|
11
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
12
|
+
url.searchParams.set(key, REDACTED);
|
|
13
|
+
}
|
|
14
|
+
url.hash = "";
|
|
15
|
+
return url.toString();
|
|
16
|
+
} catch {
|
|
17
|
+
return REDACTED;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function redactMCPDiagnosticValue(value: unknown): unknown {
|
|
22
|
+
if (Array.isArray(value)) return value.map(redactMCPDiagnosticValue);
|
|
23
|
+
if (!value || typeof value !== "object") return value;
|
|
24
|
+
return Object.fromEntries(
|
|
25
|
+
Object.entries(value).map(([key, entry]) => [
|
|
26
|
+
key,
|
|
27
|
+
SENSITIVE_KEY_PATTERN.test(key) ? REDACTED : redactMCPDiagnosticValue(entry),
|
|
28
|
+
]),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
MCP_MAX_SSE_REQUEST_MESSAGES,
|
|
25
25
|
readMCPResponseText,
|
|
26
26
|
} from "../content-limits";
|
|
27
|
+
import { fetchPluginMcpRequest, isPluginMcpPublicNetworkBound } from "../plugin-network-boundary";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* HTTP transport for MCP servers.
|
|
@@ -62,6 +63,12 @@ export class HttpTransport implements MCPTransport {
|
|
|
62
63
|
this.#connected = true;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
#fetch(init: BunFetchRequestInit): Promise<Response> {
|
|
67
|
+
return isPluginMcpPublicNetworkBound(this.config)
|
|
68
|
+
? fetchPluginMcpRequest(this.config.url, init)
|
|
69
|
+
: fetch(this.config.url, init);
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
#trackReader(promise: Promise<void>, controller?: AbortController): void {
|
|
66
73
|
if (controller) this.#streamControllers.add(controller);
|
|
67
74
|
this.#streamReaders.add(promise);
|
|
@@ -96,7 +103,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
96
103
|
|
|
97
104
|
let response: Response;
|
|
98
105
|
try {
|
|
99
|
-
response = await fetch(
|
|
106
|
+
response = await this.#fetch({
|
|
100
107
|
method: "GET",
|
|
101
108
|
headers,
|
|
102
109
|
signal: AbortSignal.any([sseConnection.signal, headerController.signal]),
|
|
@@ -227,7 +234,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
227
234
|
: abortController.signal;
|
|
228
235
|
|
|
229
236
|
try {
|
|
230
|
-
const response = await fetch(
|
|
237
|
+
const response = await this.#fetch({
|
|
231
238
|
method: "POST",
|
|
232
239
|
headers,
|
|
233
240
|
body: JSON.stringify(body),
|
|
@@ -423,7 +430,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
423
430
|
headers["Mcp-Session-Id"] = this.#sessionId;
|
|
424
431
|
}
|
|
425
432
|
try {
|
|
426
|
-
const resp = await fetch(
|
|
433
|
+
const resp = await this.#fetch({
|
|
427
434
|
method: "POST",
|
|
428
435
|
headers,
|
|
429
436
|
body: JSON.stringify(body),
|
|
@@ -437,7 +444,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
437
444
|
this.config.headers ??= {};
|
|
438
445
|
Object.assign(this.config.headers, newHeaders);
|
|
439
446
|
Object.assign(headers, newHeaders);
|
|
440
|
-
const retry = await fetch(
|
|
447
|
+
const retry = await this.#fetch({
|
|
441
448
|
method: "POST",
|
|
442
449
|
headers,
|
|
443
450
|
body: JSON.stringify(body),
|
|
@@ -480,7 +487,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
480
487
|
const timeoutId = setTimeout(() => abortController.abort(), timeout);
|
|
481
488
|
|
|
482
489
|
try {
|
|
483
|
-
const response = await fetch(
|
|
490
|
+
const response = await this.#fetch({
|
|
484
491
|
method: "POST",
|
|
485
492
|
headers,
|
|
486
493
|
body: JSON.stringify(body),
|
|
@@ -542,7 +549,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
542
549
|
"Mcp-Session-Id": this.#sessionId,
|
|
543
550
|
};
|
|
544
551
|
|
|
545
|
-
await fetch(
|
|
552
|
+
await this.#fetch({
|
|
546
553
|
method: "DELETE",
|
|
547
554
|
headers,
|
|
548
555
|
signal: AbortSignal.timeout(timeout),
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type { BigIntStats } from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
|
|
6
|
+
import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
|
|
5
7
|
import {
|
|
6
8
|
type DirectoryMigrationPolicy,
|
|
7
9
|
listManagedSessionCandidates,
|
|
@@ -141,6 +143,7 @@ export type BrokerResponse =
|
|
|
141
143
|
error: {
|
|
142
144
|
code: BrokerErrorCode;
|
|
143
145
|
message: string;
|
|
146
|
+
details?: ModelProfileErrorDetails;
|
|
144
147
|
endpoint?: "unavailable";
|
|
145
148
|
cleanup?: BrokerCleanupEvidence;
|
|
146
149
|
};
|
|
@@ -362,6 +365,7 @@ type BrokerLockSnapshot = {
|
|
|
362
365
|
ownerId?: string;
|
|
363
366
|
pid: number;
|
|
364
367
|
identity: string;
|
|
368
|
+
lockIdentity: string;
|
|
365
369
|
};
|
|
366
370
|
|
|
367
371
|
const BROKER_PUBLICATION_CADENCE_MS = 5_000;
|
|
@@ -415,7 +419,7 @@ export class Broker {
|
|
|
415
419
|
#lockRecordPath(): string {
|
|
416
420
|
return path.join(this.#lock, BROKER_LOCK_RECORD);
|
|
417
421
|
}
|
|
418
|
-
#lockSnapshot(raw: string): BrokerLockSnapshot {
|
|
422
|
+
async #lockSnapshot(raw: string, lockIdentity: string): Promise<BrokerLockSnapshot> {
|
|
419
423
|
try {
|
|
420
424
|
const lock = JSON.parse(raw) as { ownerId?: unknown; pid?: unknown };
|
|
421
425
|
if (
|
|
@@ -425,32 +429,37 @@ export class Broker {
|
|
|
425
429
|
Number.isInteger(lock.pid) &&
|
|
426
430
|
lock.pid > 0
|
|
427
431
|
)
|
|
428
|
-
return { ownerId: lock.ownerId, pid: lock.pid, identity: `owner:${lock.ownerId}
|
|
432
|
+
return { ownerId: lock.ownerId, pid: lock.pid, identity: `owner:${lock.ownerId}`, lockIdentity };
|
|
429
433
|
} catch {}
|
|
430
|
-
return { pid: 0, identity: `contents:${createHash("sha256").update(raw).digest("hex")}
|
|
434
|
+
return { pid: 0, identity: `contents:${createHash("sha256").update(raw).digest("hex")}`, lockIdentity };
|
|
431
435
|
}
|
|
432
436
|
async #readLock(): Promise<BrokerLockSnapshot | null> {
|
|
437
|
+
let lock: BigIntStats;
|
|
433
438
|
try {
|
|
434
|
-
|
|
439
|
+
lock = await fs.lstat(this.#lock, { bigint: true });
|
|
435
440
|
} catch (e) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
return this.#lockSnapshot(await fs.readFile(this.#lock, "utf8"));
|
|
440
|
-
} catch (legacyError) {
|
|
441
|
-
if ((legacyError as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
442
|
-
throw legacyError;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
if (code !== "ENOENT") throw e;
|
|
441
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
442
|
+
throw e;
|
|
446
443
|
}
|
|
444
|
+
const lockIdentity = `${lock.dev}:${lock.ino}`;
|
|
445
|
+
let raw: string;
|
|
447
446
|
try {
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
raw = lock.isDirectory()
|
|
448
|
+
? await fs.readFile(path.join(this.#lock, BROKER_LOCK_RECORD), "utf8")
|
|
449
|
+
: await fs.readFile(this.#lock, "utf8");
|
|
450
|
+
} catch (e) {
|
|
451
|
+
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
|
|
452
|
+
raw = "";
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const current = await fs.lstat(this.#lock, { bigint: true });
|
|
456
|
+
if (`${current.dev}:${current.ino}` !== lockIdentity || current.isDirectory() !== lock.isDirectory())
|
|
457
|
+
return null;
|
|
450
458
|
} catch (e) {
|
|
451
459
|
if ((e as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
452
460
|
throw e;
|
|
453
461
|
}
|
|
462
|
+
return this.#lockSnapshot(raw, lockIdentity);
|
|
454
463
|
}
|
|
455
464
|
async #createLock(): Promise<void> {
|
|
456
465
|
await fs.mkdir(this.#lock, { mode: 0o700 });
|
|
@@ -478,13 +487,18 @@ export class Broker {
|
|
|
478
487
|
}
|
|
479
488
|
async #reclaimStaleLock(snapshot: BrokerLockSnapshot): Promise<void> {
|
|
480
489
|
const current = await this.#readLock();
|
|
481
|
-
if (
|
|
490
|
+
if (
|
|
491
|
+
!current ||
|
|
492
|
+
current.identity !== snapshot.identity ||
|
|
493
|
+
current.lockIdentity !== snapshot.lockIdentity ||
|
|
494
|
+
(current.pid > 0 && isPidAlive(current.pid))
|
|
495
|
+
)
|
|
496
|
+
return;
|
|
482
497
|
|
|
483
|
-
//
|
|
484
|
-
// holding an old snapshot from renaming a newly-created directory lock.
|
|
498
|
+
// Snapshot validation and the deterministic instance suffix protect successor locks.
|
|
485
499
|
const tombstone = path.join(
|
|
486
500
|
path.dirname(this.#lock),
|
|
487
|
-
`.broker.lock.stale-${createHash("sha256").update(snapshot.
|
|
501
|
+
`.broker.lock.stale-${createHash("sha256").update(snapshot.lockIdentity).digest("hex")}`,
|
|
488
502
|
);
|
|
489
503
|
try {
|
|
490
504
|
await fs.rename(this.#lock, tombstone);
|
|
@@ -7,6 +7,13 @@ import type { NativeExactUnlinkResult } from "@gajae-code/natives";
|
|
|
7
7
|
import * as native from "@gajae-code/natives";
|
|
8
8
|
import { resolveEquivalentPath } from "@gajae-code/utils";
|
|
9
9
|
|
|
10
|
+
import {
|
|
11
|
+
isModelProfileError,
|
|
12
|
+
type ModelProfileErrorDetails,
|
|
13
|
+
validateModelProfileName,
|
|
14
|
+
} from "../../config/model-profile-contract";
|
|
15
|
+
import { mergeModelProfiles } from "../../config/model-profiles";
|
|
16
|
+
import { ModelsConfigFile } from "../../config/model-registry";
|
|
10
17
|
import {
|
|
11
18
|
ensureLaunchWorktree,
|
|
12
19
|
ensureReusableNodeModules,
|
|
@@ -326,14 +333,36 @@ function serializeCleanupIdentity(identity: CleanupIdentity): BrokerCleanupIdent
|
|
|
326
333
|
};
|
|
327
334
|
}
|
|
328
335
|
|
|
329
|
-
const fail = (
|
|
336
|
+
const fail = (
|
|
337
|
+
code: string,
|
|
338
|
+
message: string,
|
|
339
|
+
cleanup?: CleanupEvidence,
|
|
340
|
+
details?: ModelProfileErrorDetails,
|
|
341
|
+
): BrokerResponse => ({
|
|
330
342
|
ok: false,
|
|
331
|
-
error: { code: code as never, message, ...(cleanup ? { cleanup } : {}) },
|
|
343
|
+
error: { code: code as never, message, ...(details ? { details } : {}), ...(cleanup ? { cleanup } : {}) },
|
|
332
344
|
});
|
|
333
345
|
function text(value: unknown): string | undefined {
|
|
334
346
|
return typeof value === "string" && value ? value : undefined;
|
|
335
347
|
}
|
|
336
348
|
|
|
349
|
+
function validateBrokerModelPreset(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
350
|
+
const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
|
|
351
|
+
modelsConfigFile.invalidate();
|
|
352
|
+
const loaded = modelsConfigFile.tryLoad();
|
|
353
|
+
const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined);
|
|
354
|
+
try {
|
|
355
|
+
return validateModelProfileName(requestedProfile, profiles, loaded.status === "error" ? loaded.error : undefined);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (isModelProfileError(error)) return fail(error.code, error.message, undefined, error.details);
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function validateBrokerModelPresetForTest(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
363
|
+
return validateBrokerModelPreset(agentDir, requestedProfile);
|
|
364
|
+
}
|
|
365
|
+
|
|
337
366
|
function readinessTimeout(input: Input): number | BrokerResponse {
|
|
338
367
|
const value = input.readinessTimeoutMs;
|
|
339
368
|
if (value === undefined) return READY_TIMEOUT_MS;
|
|
@@ -624,7 +653,7 @@ type ReadyAuthority = {
|
|
|
624
653
|
};
|
|
625
654
|
type ReadinessResult =
|
|
626
655
|
| { kind: "ready"; authority: ReadyAuthority }
|
|
627
|
-
| { kind: "startup_failed";
|
|
656
|
+
| { kind: "startup_failed"; failure: SdkStartupFailure }
|
|
628
657
|
| { kind: "child_exited" }
|
|
629
658
|
| { kind: "timeout" };
|
|
630
659
|
const processIncarnationReadersForTest = new WeakMap<Broker, (pid: number) => string | undefined>();
|
|
@@ -797,19 +826,25 @@ function isLifecycleTranscriptEvidence(value: unknown): value is LifecycleTransc
|
|
|
797
826
|
function isSdkStartupFailure(value: unknown): value is SdkStartupFailure {
|
|
798
827
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
799
828
|
const failure = value as Record<string, unknown>;
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
(
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
failure.reason
|
|
808
|
-
failure.reason
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
829
|
+
const keys = Object.keys(failure);
|
|
830
|
+
if (
|
|
831
|
+
!keys.every(
|
|
832
|
+
key => key === "phase" || key === "reason" || key === "message" || key === "code" || key === "details",
|
|
833
|
+
) ||
|
|
834
|
+
(failure.phase !== "registration" && failure.phase !== "startup") ||
|
|
835
|
+
(failure.reason !== "disabled" &&
|
|
836
|
+
failure.reason !== "ineligible" &&
|
|
837
|
+
failure.reason !== "factory_absent" &&
|
|
838
|
+
failure.reason !== "runner_absent" &&
|
|
839
|
+
failure.reason !== "pending" &&
|
|
840
|
+
failure.reason !== "failed") ||
|
|
841
|
+
typeof failure.message !== "string" ||
|
|
842
|
+
Buffer.byteLength(failure.message) === 0 ||
|
|
843
|
+
Buffer.byteLength(failure.message) > 512
|
|
844
|
+
)
|
|
845
|
+
return false;
|
|
846
|
+
if (failure.code === undefined && failure.details === undefined) return keys.length === 3;
|
|
847
|
+
return keys.length === 5 && isModelProfileError(failure);
|
|
813
848
|
}
|
|
814
849
|
|
|
815
850
|
function isLifecycleFailureArtifact(value: unknown): value is LifecycleFailureArtifact {
|
|
@@ -818,8 +853,14 @@ function isLifecycleFailureArtifact(value: unknown): value is LifecycleFailureAr
|
|
|
818
853
|
if (!isEffectMarker(record)) return false;
|
|
819
854
|
const artifact = value as LifecycleFailureArtifact;
|
|
820
855
|
return (
|
|
821
|
-
Object.keys(record).length ===
|
|
822
|
-
|
|
856
|
+
Object.keys(record).length ===
|
|
857
|
+
7 + (artifact.code === undefined ? 0 : 2) + (artifact.transcript === undefined ? 0 : 1) &&
|
|
858
|
+
isSdkStartupFailure({
|
|
859
|
+
phase: artifact.phase,
|
|
860
|
+
reason: artifact.reason,
|
|
861
|
+
message: artifact.message,
|
|
862
|
+
...(artifact.code === undefined ? {} : { code: artifact.code, details: artifact.details }),
|
|
863
|
+
}) &&
|
|
823
864
|
isRollbackResult(artifact.rollback) &&
|
|
824
865
|
(artifact.transcript === undefined || isLifecycleTranscriptEvidence(artifact.transcript))
|
|
825
866
|
);
|
|
@@ -865,6 +906,8 @@ export async function writeSessionLifecycleFailure(
|
|
|
865
906
|
...(transcript ? { transcript } : {}),
|
|
866
907
|
};
|
|
867
908
|
const bytes = Buffer.from(canonicalJson(artifact), "utf8");
|
|
909
|
+
if (bytes.length > MAX_LIFECYCLE_METADATA_BYTES)
|
|
910
|
+
throw new Error("Lifecycle startup failure exceeds the metadata size ceiling.");
|
|
868
911
|
const target = lifecycleFailurePath(root, id, effectMarker);
|
|
869
912
|
const temporary = path.join(directory, `.${id}.lifecycle.failure.${effectMarker}.${randomUUID()}.tmp`);
|
|
870
913
|
let published = false;
|
|
@@ -1241,6 +1284,10 @@ function validateLifecycleMetadataReplay(cleanup: CleanupEvidence): BrokerRespon
|
|
|
1241
1284
|
if (!current) continue;
|
|
1242
1285
|
if (!sameLifecycleCleanupIdentity(current.identity, file.identity))
|
|
1243
1286
|
return fail("terminal_uncertain", "Lifecycle metadata candidate lacks exact replay authority.");
|
|
1287
|
+
// A completed file's recorded retained quarantine is receipt-bound durable
|
|
1288
|
+
// evidence; anything else that remains is an active survivor.
|
|
1289
|
+
if (file.completed && file.detachedPath && path.resolve(candidate) === path.resolve(file.detachedPath))
|
|
1290
|
+
continue;
|
|
1244
1291
|
activeCandidates++;
|
|
1245
1292
|
}
|
|
1246
1293
|
if (file.completed && activeCandidates > 0)
|
|
@@ -1289,7 +1336,21 @@ function lifecycleMetadataReplayAbsent(cleanup: CleanupEvidence): boolean {
|
|
|
1289
1336
|
const id = cleanup.sessionId!;
|
|
1290
1337
|
const required = new Set<string>([lifecycleMarkerPath(root, id), lifecycleReadyPath(root, id)]);
|
|
1291
1338
|
for (const file of files) for (const candidate of lifecycleCleanupCandidates(file)) required.add(candidate);
|
|
1292
|
-
|
|
1339
|
+
for (const candidate of required) {
|
|
1340
|
+
if (absentLifecyclePath(candidate)) continue;
|
|
1341
|
+
// A completed file's recorded retained quarantine is durable evidence, not a
|
|
1342
|
+
// survivor — accept it only at its receipt-bound path and identity.
|
|
1343
|
+
const owner = files.find(
|
|
1344
|
+
file =>
|
|
1345
|
+
file.completed === true &&
|
|
1346
|
+
file.detachedPath !== undefined &&
|
|
1347
|
+
path.resolve(file.detachedPath) === path.resolve(candidate),
|
|
1348
|
+
);
|
|
1349
|
+
if (!owner) return false;
|
|
1350
|
+
const current = captureLifecycleFile(candidate, true, true);
|
|
1351
|
+
if (!current || !sameLifecycleCleanupIdentity(current.identity, owner.identity)) return false;
|
|
1352
|
+
}
|
|
1353
|
+
return true;
|
|
1293
1354
|
}
|
|
1294
1355
|
|
|
1295
1356
|
/**
|
|
@@ -1552,16 +1613,28 @@ async function reconcileLifecycleCleanup(
|
|
|
1552
1613
|
const candidates = lifecycleCleanupCandidates(file);
|
|
1553
1614
|
if (file.completed) {
|
|
1554
1615
|
for (const candidate of candidates) {
|
|
1616
|
+
let stat: ReturnType<typeof fsSync.lstatSync>;
|
|
1555
1617
|
try {
|
|
1556
|
-
fsSync.lstatSync(candidate);
|
|
1557
|
-
return fail(
|
|
1558
|
-
"terminal_uncertain",
|
|
1559
|
-
"Lifecycle cleanup receipt marks a target complete while an authorized candidate remains.",
|
|
1560
|
-
);
|
|
1618
|
+
stat = fsSync.lstatSync(candidate);
|
|
1561
1619
|
} catch (error) {
|
|
1562
|
-
if ((error as NodeJS.ErrnoException).code
|
|
1563
|
-
|
|
1620
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1621
|
+
return fail("terminal_uncertain", "Lifecycle cleanup completion could not be safely inspected.");
|
|
1622
|
+
}
|
|
1623
|
+
// A completed file's recorded retained quarantine is durable evidence,
|
|
1624
|
+
// not a survivor — accept it only at its receipt-bound path and identity.
|
|
1625
|
+
if (
|
|
1626
|
+
file.detachedPath &&
|
|
1627
|
+
path.resolve(candidate) === path.resolve(file.detachedPath) &&
|
|
1628
|
+
stat.isFile() &&
|
|
1629
|
+
!stat.isSymbolicLink()
|
|
1630
|
+
) {
|
|
1631
|
+
const current = captureLifecycleFile(candidate, true, true);
|
|
1632
|
+
if (current && sameLifecycleCleanupIdentity(current.identity, file.identity)) continue;
|
|
1564
1633
|
}
|
|
1634
|
+
return fail(
|
|
1635
|
+
"terminal_uncertain",
|
|
1636
|
+
"Lifecycle cleanup receipt marks a target complete while an authorized candidate remains.",
|
|
1637
|
+
);
|
|
1565
1638
|
}
|
|
1566
1639
|
continue;
|
|
1567
1640
|
}
|
|
@@ -1631,6 +1704,22 @@ async function reconcileLifecycleCleanup(
|
|
|
1631
1704
|
quarantineName: path.basename(currentFile.plannedPath),
|
|
1632
1705
|
});
|
|
1633
1706
|
if (!result.ok) {
|
|
1707
|
+
if (result.code === "cleanup_pending" && result.detachedPath === currentFile.plannedPath) {
|
|
1708
|
+
// Typed retained authority: the native verified the exact identity-bound
|
|
1709
|
+
// detach and retained the quarantine as durable evidence. Record the
|
|
1710
|
+
// evidence and advance — never claim a terminal byte deletion.
|
|
1711
|
+
const lifecycleFiles = activeCleanup.lifecycleFiles!.map((candidate, candidateIndex) =>
|
|
1712
|
+
candidateIndex === index
|
|
1713
|
+
? { ...candidate, detachedPath: result.detachedPath, completed: true as const }
|
|
1714
|
+
: candidate,
|
|
1715
|
+
);
|
|
1716
|
+
activeCleanup = { ...activeCleanup, lifecycleFiles };
|
|
1717
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
1718
|
+
response: fail("cleanup_pending", "Lifecycle cleanup completion was durably reconciled.", activeCleanup),
|
|
1719
|
+
});
|
|
1720
|
+
lifecycleCleanupHooksForTest.get(broker)?.();
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1634
1723
|
const lifecycleFiles = activeCleanup.lifecycleFiles!.map((candidate, candidateIndex) =>
|
|
1635
1724
|
candidateIndex === index && result.detachedPath
|
|
1636
1725
|
? { ...candidate, detachedPath: result.detachedPath }
|
|
@@ -1665,6 +1754,22 @@ async function readSessionLifecycleFailure(
|
|
|
1665
1754
|
?.artifact;
|
|
1666
1755
|
}
|
|
1667
1756
|
|
|
1757
|
+
export async function readSessionLifecycleFailureForTest(
|
|
1758
|
+
root: string,
|
|
1759
|
+
id: string,
|
|
1760
|
+
expected: { pid: number; effectMarker: string; incarnation: string },
|
|
1761
|
+
): Promise<SdkStartupFailure | undefined> {
|
|
1762
|
+
const artifact = await readSessionLifecycleFailure(root, id, expected);
|
|
1763
|
+
return artifact
|
|
1764
|
+
? {
|
|
1765
|
+
phase: artifact.phase,
|
|
1766
|
+
reason: artifact.reason,
|
|
1767
|
+
message: artifact.message,
|
|
1768
|
+
...(artifact.code === undefined ? {} : { code: artifact.code, details: artifact.details }),
|
|
1769
|
+
}
|
|
1770
|
+
: undefined;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1668
1773
|
async function hasDurableProcessIdentity(
|
|
1669
1774
|
root: string,
|
|
1670
1775
|
id: string,
|
|
@@ -2056,14 +2161,14 @@ async function waitForReady(
|
|
|
2056
2161
|
): Promise<ReadinessResult> {
|
|
2057
2162
|
while (timing.now() < deadline) {
|
|
2058
2163
|
const startupFailure = await readSessionLifecycleFailure(root, id, expected);
|
|
2059
|
-
if (startupFailure) return { kind: "startup_failed",
|
|
2164
|
+
if (startupFailure) return { kind: "startup_failed", failure: startupFailure };
|
|
2060
2165
|
if (
|
|
2061
2166
|
observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
|
|
2062
2167
|
"exited"
|
|
2063
2168
|
) {
|
|
2064
2169
|
const finalStartupFailure = await readSessionLifecycleFailure(root, id, expected);
|
|
2065
2170
|
return finalStartupFailure
|
|
2066
|
-
? { kind: "startup_failed",
|
|
2171
|
+
? { kind: "startup_failed", failure: finalStartupFailure }
|
|
2067
2172
|
: { kind: "child_exited" };
|
|
2068
2173
|
}
|
|
2069
2174
|
try {
|
|
@@ -2191,6 +2296,8 @@ async function launchInput(
|
|
|
2191
2296
|
const requested = sessionId(input);
|
|
2192
2297
|
if (requested !== undefined && !isCanonicalSessionId(requested))
|
|
2193
2298
|
return fail("invalid_input", "sessionId must be a canonical safe identifier.");
|
|
2299
|
+
if (input.modelPreset !== undefined && (typeof input.modelPreset !== "string" || input.modelPreset.length === 0))
|
|
2300
|
+
return fail("invalid_input", "modelPreset must be a non-empty exact profile ID.");
|
|
2194
2301
|
const modelPreset = text(input.modelPreset);
|
|
2195
2302
|
|
|
2196
2303
|
if (operation === "session.create")
|
|
@@ -2615,6 +2722,11 @@ async function executeLifecycleResponse(
|
|
|
2615
2722
|
|
|
2616
2723
|
const launch = await launchInput(broker, operation, input);
|
|
2617
2724
|
if ("ok" in launch) return launch;
|
|
2725
|
+
if (launch.modelPreset) {
|
|
2726
|
+
const validatedModelPreset = validateBrokerModelPreset(broker.settings.agentDir, launch.modelPreset);
|
|
2727
|
+
if (typeof validatedModelPreset !== "string") return validatedModelPreset;
|
|
2728
|
+
launch.modelPreset = validatedModelPreset;
|
|
2729
|
+
}
|
|
2618
2730
|
if (!hasProcessIncarnationAuthority())
|
|
2619
2731
|
return fail(
|
|
2620
2732
|
"incarnation_unavailable",
|
|
@@ -2768,7 +2880,12 @@ async function executeLifecycleResponse(
|
|
|
2768
2880
|
`Session ${launch.id} did not become ready and its spawned process could not be verified dead.`,
|
|
2769
2881
|
);
|
|
2770
2882
|
return readiness.kind === "startup_failed"
|
|
2771
|
-
? fail(
|
|
2883
|
+
? fail(
|
|
2884
|
+
readiness.failure.code ?? "spawn_failed",
|
|
2885
|
+
readiness.failure.message,
|
|
2886
|
+
undefined,
|
|
2887
|
+
readiness.failure.details,
|
|
2888
|
+
)
|
|
2772
2889
|
: readiness.kind === "child_exited"
|
|
2773
2890
|
? fail("spawn_failed", `Session ${launch.id} exited before registering readiness.`)
|
|
2774
2891
|
: fail(
|
|
@@ -3043,11 +3160,26 @@ async function executeLifecycleResponse(
|
|
|
3043
3160
|
`Unable to delete saved session artifacts: ${error instanceof Error ? error.message : String(error)}`,
|
|
3044
3161
|
);
|
|
3045
3162
|
}
|
|
3046
|
-
|
|
3163
|
+
const retainedArtifactsCleanup =
|
|
3164
|
+
deleted.kind === "cleanup_pending" &&
|
|
3165
|
+
deleted.phase === "artifacts" &&
|
|
3166
|
+
deleted.detachedArtifactsPath !== undefined &&
|
|
3167
|
+
cleanupTarget.plannedArtifactsPath !== undefined &&
|
|
3168
|
+
(deleted.detachedArtifactsPath === cleanupTarget.plannedArtifactsPath ||
|
|
3169
|
+
deleted.detachedArtifactsPath === `${cleanupTarget.plannedArtifactsPath}.removing`);
|
|
3170
|
+
if (deleted.kind === "artifacts_removed" || retainedArtifactsCleanup) {
|
|
3047
3171
|
const transcriptPhaseCleanup = {
|
|
3048
3172
|
...preauthorizedCleanup,
|
|
3049
3173
|
phase: "transcript" as const,
|
|
3050
3174
|
artifactsRemoved: true,
|
|
3175
|
+
...(deleted.kind === "cleanup_pending" && deleted.phase === "artifacts"
|
|
3176
|
+
? {
|
|
3177
|
+
detachedArtifactsPath: deleted.detachedArtifactsPath,
|
|
3178
|
+
...(deleted.artifactsIdentity
|
|
3179
|
+
? { artifactsIdentity: serializeCleanupIdentity(deleted.artifactsIdentity) }
|
|
3180
|
+
: {}),
|
|
3181
|
+
}
|
|
3182
|
+
: {}),
|
|
3051
3183
|
...(preauthorizedCleanup.artifactTree
|
|
3052
3184
|
? { artifactTree: { ...preauthorizedCleanup.artifactTree, completed: true as const } }
|
|
3053
3185
|
: {}),
|
|
@@ -3068,7 +3200,14 @@ async function executeLifecycleResponse(
|
|
|
3068
3200
|
artifactsRemoved: true,
|
|
3069
3201
|
});
|
|
3070
3202
|
}
|
|
3071
|
-
if (
|
|
3203
|
+
if (
|
|
3204
|
+
deleted.kind === "cleanup_pending" &&
|
|
3205
|
+
!(
|
|
3206
|
+
deleted.phase === "transcript" &&
|
|
3207
|
+
deleted.detachedTranscriptPath !== undefined &&
|
|
3208
|
+
deleted.detachedTranscriptPath === cleanupTarget.plannedTranscriptPath
|
|
3209
|
+
)
|
|
3210
|
+
)
|
|
3072
3211
|
return fail(
|
|
3073
3212
|
"cleanup_pending",
|
|
3074
3213
|
`Saved session cleanup is pending in ${deleted.phase}: ${deleted.error.message}`,
|
|
@@ -3323,6 +3462,9 @@ export async function executeLifecycle(
|
|
|
3323
3462
|
phase: evidence.artifact.phase,
|
|
3324
3463
|
reason: evidence.artifact.reason,
|
|
3325
3464
|
message: evidence.artifact.message,
|
|
3465
|
+
...(evidence.artifact.code === undefined
|
|
3466
|
+
? {}
|
|
3467
|
+
: { code: evidence.artifact.code, details: evidence.artifact.details }),
|
|
3326
3468
|
rollback: {
|
|
3327
3469
|
endpointGeneration: evidence.artifact.rollback.endpointGeneration,
|
|
3328
3470
|
fenced: evidence.artifact.rollback.fenced,
|
|
@@ -3387,11 +3529,18 @@ export async function executeLifecycle(
|
|
|
3387
3529
|
: startupFailure && cleanupProof
|
|
3388
3530
|
? {
|
|
3389
3531
|
ok: false,
|
|
3390
|
-
error:
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3532
|
+
error: startupFailure.code
|
|
3533
|
+
? {
|
|
3534
|
+
code: startupFailure.code,
|
|
3535
|
+
message: startupFailure.message,
|
|
3536
|
+
details: startupFailure.details!,
|
|
3537
|
+
endpoint: "unavailable" as const,
|
|
3538
|
+
}
|
|
3539
|
+
: {
|
|
3540
|
+
code: "spawn_failed",
|
|
3541
|
+
message: "No ready SDK endpoint remains available.",
|
|
3542
|
+
endpoint: "unavailable" as const,
|
|
3543
|
+
},
|
|
3395
3544
|
...(durableEffects ? { durableEffects } : {}),
|
|
3396
3545
|
startupFailure,
|
|
3397
3546
|
}
|
|
@@ -131,6 +131,7 @@ function compactEvents(events: SessionIndexEvent[]): SessionIndexEvent[] {
|
|
|
131
131
|
|
|
132
132
|
async function appendSync(file: string, value: string): Promise<void> {
|
|
133
133
|
const h = await fs.open(file, "a", 0o600);
|
|
134
|
+
let failure: { error: unknown } | undefined;
|
|
134
135
|
try {
|
|
135
136
|
const data = Buffer.from(`${value}\n`);
|
|
136
137
|
for (let offset = 0; offset < data.length; ) {
|
|
@@ -139,9 +140,18 @@ async function appendSync(file: string, value: string): Promise<void> {
|
|
|
139
140
|
offset += bytesWritten;
|
|
140
141
|
}
|
|
141
142
|
await h.sync();
|
|
142
|
-
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
failure = { error };
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
143
147
|
await h.close();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// Bun may report EBADF when concurrent child-pipe teardown has already
|
|
150
|
+
// released a fully written and fsynced append handle. The descriptor is
|
|
151
|
+
// closed in that case; every other close failure remains fatal.
|
|
152
|
+
if ((error as NodeJS.ErrnoException).code !== "EBADF" && failure === undefined) failure = { error };
|
|
144
153
|
}
|
|
154
|
+
if (failure !== undefined) throw failure.error;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
async function syncDirectory(file: string): Promise<void> {
|
|
@@ -28,11 +28,17 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
28
28
|
* Generation 7 restores macOS daemon signaling (kill(2) with a start-time
|
|
29
29
|
* incarnation recheck) so a live/hung owner can be replaced without an external
|
|
30
30
|
* `kill -9`. Generation 8 adopts Windows expected-identity ACL verification and
|
|
31
|
-
* repair for shared native authority.
|
|
31
|
+
* repair for shared native authority. Generation 9 refreshes the shared native
|
|
32
|
+
* authority declaration contract with bounded frame-delivery acknowledgement.
|
|
33
|
+
* Generation 11 accepts typed retained exact-unlink cleanup authority — a
|
|
34
|
+
* concrete detached quarantine plus a proven-absent canonical lock pathname —
|
|
35
|
+
* when deleting an observed owner-lock lease. Generation 12 refreshes retained
|
|
36
|
+
* native path and process authority semantics. Generation 13 rejects special
|
|
37
|
+
* files before retained native authority opens.
|
|
32
38
|
*/
|
|
33
39
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
34
|
-
discord:
|
|
35
|
-
slack:
|
|
40
|
+
discord: 13,
|
|
41
|
+
slack: 13,
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -938,10 +944,19 @@ async function ownsChatDaemonOwnerLock(lock: string, lease: ChatDaemonOwnerLockL
|
|
|
938
944
|
/** Deletes only the exact lease observed by this contender; a successor is retained. */
|
|
939
945
|
function unlinkExactChatDaemonOwnerLock(lock: string, lease: ChatDaemonOwnerLockLease): boolean {
|
|
940
946
|
try {
|
|
941
|
-
|
|
947
|
+
const removed = native.exactUnlink(lock, {
|
|
942
948
|
...lease,
|
|
943
949
|
quarantineName: `.gjc-delete-chat-daemon-lock-${crypto.randomUUID()}`,
|
|
944
|
-
})
|
|
950
|
+
});
|
|
951
|
+
if (removed.ok) return true;
|
|
952
|
+
// Accept only typed retained authority: a concrete detached quarantine plus
|
|
953
|
+
// a proven-absent canonical lock pathname. Anything else stays fail-closed.
|
|
954
|
+
return (
|
|
955
|
+
removed.code === "cleanup_pending" &&
|
|
956
|
+
typeof removed.detachedPath === "string" &&
|
|
957
|
+
removed.detachedPath.length > 0 &&
|
|
958
|
+
!fs.existsSync(lock)
|
|
959
|
+
);
|
|
945
960
|
} catch {
|
|
946
961
|
return false;
|
|
947
962
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* removed), but the user can still adjust per-surface behaviour from inside a
|
|
6
6
|
* session thread with small slash commands:
|
|
7
7
|
*
|
|
8
|
-
* - `/verbose` switch the mirror to
|
|
9
|
-
* - `/lean`
|
|
8
|
+
* - `/verbose` switch the mirror to per-turn assistant text + tool summaries + reasoning summaries
|
|
9
|
+
* - `/lean` settled assistant answer at idle + immediate ask lead-ins (no intermediate tool-turn flood)
|
|
10
10
|
* - `/verbosity lean|verbose`
|
|
11
11
|
* - `/redact on|off` toggle redaction of streamed content
|
|
12
12
|
*
|