@sayknow-cli/coding-agent 0.5.1 → 0.5.6
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 +185 -14
- package/dist/types/cli/update-cli.d.ts +8 -1
- package/dist/types/commands/session.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/telegram-autostart.d.ts +9 -1
- package/dist/types/dap/client.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +5 -0
- package/dist/types/modes/components/pet-capability.d.ts +8 -7
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +13 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +9 -0
- package/dist/types/session/session-storage.d.ts +17 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +22 -0
- package/dist/types/session-import/command.d.ts +20 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +28 -0
- package/dist/types/session-import/types.d.ts +125 -0
- package/dist/types/skc-runtime/boot-generation.d.ts +59 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +10 -2
- package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
- package/dist/types/skc-runtime/session-restore-runtime.d.ts +41 -0
- package/dist/types/skc-runtime/session-restore.d.ts +99 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +160 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +26 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
- package/dist/types/slash-commands/types.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +169 -4
- package/package.json +16 -7
- package/scripts/generate-sdk-operation-inventory.ts +5 -6
- package/src/cli/config-cli.ts +23 -8
- package/src/cli/update-cli.ts +265 -21
- package/src/commands/session.ts +88 -2
- package/src/config/atomic-yaml-patch.ts +167 -30
- package/src/config/keybindings.ts +10 -0
- package/src/config/model-profiles.ts +69 -1
- package/src/config/model-registry.ts +72 -38
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +9 -0
- package/src/config/settings.ts +13 -2
- package/src/config/telegram-autostart.ts +11 -4
- package/src/dap/client.ts +78 -30
- package/src/defaults/skc/skills/deep-interview/SKILL.md +29 -3
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/lsp/client.ts +17 -29
- package/src/main.ts +1 -1
- package/src/modes/action-registry.ts +2 -0
- package/src/modes/components/pet-capability.ts +22 -13
- package/src/modes/components/pet-selector.ts +1 -1
- package/src/modes/components/sayknow-pet-widget.ts +41 -7
- package/src/modes/components/welcome.ts +5 -0
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +71 -6
- package/src/modes/controllers/plan-mode-controller.ts +8 -1
- package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
- package/src/modes/controllers/selector-controller.ts +47 -19
- package/src/modes/interactive-mode.ts +54 -4
- package/src/modes/shared/agent-wire/unattended-session.ts +40 -9
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -0
- package/src/modes/types.ts +5 -1
- package/src/notifications/lifecycle-control-runtime.ts +258 -179
- package/src/prompts/system/eager-todo.md +2 -0
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/system-prompt.md +4 -2
- package/src/runtime-mcp/transports/stdio.ts +96 -33
- package/src/sdk/broker/lifecycle.ts +6 -5
- package/src/sdk/bus/lifecycle-control-runtime.ts +189 -110
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +25 -18
- package/src/session/agent-session.ts +120 -25
- package/src/session/internal/managed-session-scope.ts +41 -6
- package/src/session/internal/managed-session-storage.ts +14 -0
- package/src/session/session-manager.ts +113 -11
- package/src/session/session-storage.ts +70 -0
- package/src/session-import/claude.ts +382 -0
- package/src/session-import/codex.ts +458 -0
- package/src/session-import/command.ts +61 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +27 -0
- package/src/session-import/redact.ts +125 -0
- package/src/session-import/service.ts +749 -0
- package/src/session-import/types.ts +163 -0
- package/src/skc-runtime/boot-generation.ts +172 -0
- package/src/skc-runtime/launch-tmux.ts +219 -41
- package/src/skc-runtime/launch-worktree.ts +277 -4
- package/src/skc-runtime/session-restore-runtime.ts +120 -0
- package/src/skc-runtime/session-restore.ts +296 -0
- package/src/skc-runtime/session-state-sidecar.ts +41 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +665 -0
- package/src/skc-runtime/tmux-sessions.ts +284 -108
- package/src/slash-commands/acp-builtins.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +109 -5
- package/src/slash-commands/types.ts +5 -0
- package/src/tools/ask.ts +188 -10
- package/src/tools/eval.ts +2 -2
- package/src/export/html/template.css +0 -1060
- package/src/export/html/template.html +0 -47
- package/src/export/html/template.js +0 -2348
- package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
- package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
- package/vendor/insane-search/engine/tests/test_u1.py +0 -200
- package/vendor/insane-search/engine/tests/test_u4.py +0 -131
- package/vendor/insane-search/engine/tests/test_u5.py +0 -163
- package/vendor/insane-search/engine/tests/test_u7.py +0 -124
- package/vendor/insane-search/engine/tests/test_u8.py +0 -216
|
@@ -24,6 +24,12 @@ import { MCPExpectedFailure, toJsonRpcError } from "../../runtime-mcp/types";
|
|
|
24
24
|
*/
|
|
25
25
|
const CLOSE_WAIT_MS = 1_000;
|
|
26
26
|
|
|
27
|
+
/** Write one JSONL frame while honoring Bun FileSink backpressure and flush failures. */
|
|
28
|
+
export async function writeStdioFrame(stdin: Bun.FileSink, message: string): Promise<void> {
|
|
29
|
+
await Promise.resolve(stdin.write(message));
|
|
30
|
+
await stdin.flush();
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
34
|
* Build a minimal environment for a no-inherit stdio MCP child. Only OS-level
|
|
29
35
|
* keys needed to locate/run an interpreter (PATH, HOME, temp, locale, and the
|
|
@@ -69,6 +75,9 @@ export class StdioTransport implements MCPTransport {
|
|
|
69
75
|
#readLoop: Promise<void> | null = null;
|
|
70
76
|
#stderrLoop: Promise<void> | null = null;
|
|
71
77
|
#closePromise: Promise<void> | null = null;
|
|
78
|
+
#writeQueue: Promise<void> = Promise.resolve();
|
|
79
|
+
#connectionEpoch = 0;
|
|
80
|
+
#terminalFailure: { epoch: number; error: MCPExpectedFailure } | undefined;
|
|
72
81
|
|
|
73
82
|
onClose?: () => void;
|
|
74
83
|
onError?: (error: Error) => void;
|
|
@@ -115,6 +124,8 @@ export class StdioTransport implements MCPTransport {
|
|
|
115
124
|
throw new MCPExpectedFailure(error);
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
this.#connectionEpoch++;
|
|
128
|
+
this.#terminalFailure = undefined;
|
|
118
129
|
this.#connected = true;
|
|
119
130
|
|
|
120
131
|
// Start reading stdout
|
|
@@ -126,6 +137,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
126
137
|
|
|
127
138
|
async #startReadLoop(): Promise<void> {
|
|
128
139
|
if (!this.#process?.child.stdout) return;
|
|
140
|
+
const epoch = this.#connectionEpoch;
|
|
129
141
|
let failure: MCPExpectedFailure | undefined;
|
|
130
142
|
try {
|
|
131
143
|
for await (const line of readJsonl(this.#process.child.stdout)) {
|
|
@@ -138,11 +150,12 @@ export class StdioTransport implements MCPTransport {
|
|
|
138
150
|
}
|
|
139
151
|
} catch (error) {
|
|
140
152
|
failure = new MCPExpectedFailure(error);
|
|
141
|
-
if (this.#connected) {
|
|
142
|
-
this.onError?.(failure);
|
|
143
|
-
}
|
|
144
153
|
} finally {
|
|
145
|
-
|
|
154
|
+
if (failure) {
|
|
155
|
+
this.#terminalizeFailure(failure, epoch);
|
|
156
|
+
} else {
|
|
157
|
+
this.#handleClose();
|
|
158
|
+
}
|
|
146
159
|
}
|
|
147
160
|
}
|
|
148
161
|
|
|
@@ -177,7 +190,10 @@ export class StdioTransport implements MCPTransport {
|
|
|
177
190
|
}
|
|
178
191
|
// Server-to-client request: has both method and id
|
|
179
192
|
if ("method" in message && "id" in message && message.id != null) {
|
|
180
|
-
|
|
193
|
+
const stdin = this.#getStdin();
|
|
194
|
+
if (stdin) {
|
|
195
|
+
void this.#handleServerRequest(message as JsonRpcRequest, this.#connectionEpoch, stdin);
|
|
196
|
+
}
|
|
181
197
|
return;
|
|
182
198
|
}
|
|
183
199
|
|
|
@@ -211,21 +227,23 @@ export class StdioTransport implements MCPTransport {
|
|
|
211
227
|
}
|
|
212
228
|
}
|
|
213
229
|
|
|
214
|
-
async #handleServerRequest(request: JsonRpcRequest): Promise<void> {
|
|
230
|
+
async #handleServerRequest(request: JsonRpcRequest, epoch: number, stdin: Bun.FileSink): Promise<void> {
|
|
231
|
+
if (!this.onRequest) {
|
|
232
|
+
await this.#sendResponseOrClose(request.id, epoch, stdin, undefined, {
|
|
233
|
+
code: -32601,
|
|
234
|
+
message: "Method not found",
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let result: unknown;
|
|
215
240
|
try {
|
|
216
|
-
|
|
217
|
-
this.#sendResponse(request.id, undefined, { code: -32601, message: "Method not found" });
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const result = await this.onRequest(request.method, request.params);
|
|
221
|
-
this.#sendResponse(request.id, result);
|
|
241
|
+
result = await this.onRequest(request.method, request.params);
|
|
222
242
|
} catch (error) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} catch {
|
|
226
|
-
// Best-effort — process may have exited
|
|
227
|
-
}
|
|
243
|
+
await this.#sendResponseOrClose(request.id, epoch, stdin, undefined, toJsonRpcError(error));
|
|
244
|
+
return;
|
|
228
245
|
}
|
|
246
|
+
await this.#sendResponseOrClose(request.id, epoch, stdin, result);
|
|
229
247
|
}
|
|
230
248
|
|
|
231
249
|
#getStdin(): Bun.FileSink | null {
|
|
@@ -233,28 +251,70 @@ export class StdioTransport implements MCPTransport {
|
|
|
233
251
|
return typeof stdin === "object" && stdin !== null ? stdin : null;
|
|
234
252
|
}
|
|
235
253
|
|
|
236
|
-
#sendResponse(
|
|
237
|
-
|
|
238
|
-
|
|
254
|
+
async #sendResponse(
|
|
255
|
+
id: string | number,
|
|
256
|
+
epoch: number,
|
|
257
|
+
stdin: Bun.FileSink,
|
|
258
|
+
result?: unknown,
|
|
259
|
+
error?: JsonRpcError,
|
|
260
|
+
): Promise<void> {
|
|
239
261
|
const response = error
|
|
240
262
|
? { jsonrpc: "2.0" as const, id, error }
|
|
241
263
|
: { jsonrpc: "2.0" as const, id, result: result ?? {} };
|
|
242
|
-
stdin
|
|
243
|
-
|
|
264
|
+
await this.#queueWriteFrame(stdin, `${JSON.stringify(response)}\n`, epoch);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#queueWriteFrame(stdin: Bun.FileSink, message: string, epoch: number): Promise<void> {
|
|
268
|
+
const write = this.#writeQueue
|
|
269
|
+
.catch(() => {})
|
|
270
|
+
.then(() => {
|
|
271
|
+
if (!this.#connected || this.#connectionEpoch !== epoch || this.#getStdin() !== stdin) {
|
|
272
|
+
throw this.#terminalFailure?.epoch === epoch ? this.#terminalFailure.error : new MCPExpectedFailure();
|
|
273
|
+
}
|
|
274
|
+
return writeStdioFrame(stdin, message);
|
|
275
|
+
});
|
|
276
|
+
this.#writeQueue = write.catch(() => {});
|
|
277
|
+
return write;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async #sendResponseOrClose(
|
|
281
|
+
id: string | number,
|
|
282
|
+
epoch: number,
|
|
283
|
+
stdin: Bun.FileSink,
|
|
284
|
+
result?: unknown,
|
|
285
|
+
error?: JsonRpcError,
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
try {
|
|
288
|
+
await this.#sendResponse(id, epoch, stdin, result, error);
|
|
289
|
+
} catch (writeError) {
|
|
290
|
+
this.#terminalizeFailure(writeError, epoch);
|
|
291
|
+
}
|
|
244
292
|
}
|
|
245
293
|
|
|
246
294
|
#handleClose(failure?: MCPExpectedFailure): void {
|
|
247
295
|
void this.#closeInternal(true, failure);
|
|
248
296
|
}
|
|
249
297
|
|
|
298
|
+
#terminalizeFailure(error: unknown, epoch: number): MCPExpectedFailure {
|
|
299
|
+
const existing = this.#terminalFailure;
|
|
300
|
+
if (existing?.epoch === epoch) return existing.error;
|
|
301
|
+
const failure = error instanceof MCPExpectedFailure ? error : new MCPExpectedFailure(error);
|
|
302
|
+
if (epoch !== this.#connectionEpoch) return failure;
|
|
303
|
+
this.#terminalFailure = { epoch, error: failure };
|
|
304
|
+
this.onError?.(failure);
|
|
305
|
+
this.#handleClose(failure);
|
|
306
|
+
return failure;
|
|
307
|
+
}
|
|
308
|
+
|
|
250
309
|
async request<T = unknown>(
|
|
251
310
|
method: string,
|
|
252
311
|
params?: Record<string, unknown>,
|
|
253
312
|
options?: MCPRequestOptions,
|
|
254
313
|
): Promise<T> {
|
|
255
314
|
const stdin = this.#getStdin();
|
|
315
|
+
const epoch = this.#connectionEpoch;
|
|
256
316
|
if (!this.#connected || !stdin) {
|
|
257
|
-
throw new MCPExpectedFailure();
|
|
317
|
+
throw this.#terminalFailure?.epoch === epoch ? this.#terminalFailure.error : new MCPExpectedFailure();
|
|
258
318
|
}
|
|
259
319
|
|
|
260
320
|
const id = Snowflake.next();
|
|
@@ -318,12 +378,10 @@ export class StdioTransport implements MCPTransport {
|
|
|
318
378
|
|
|
319
379
|
const message = `${JSON.stringify(request)}\n`;
|
|
320
380
|
try {
|
|
321
|
-
|
|
322
|
-
stdin.write(message);
|
|
323
|
-
stdin.flush();
|
|
381
|
+
await this.#queueWriteFrame(stdin, message, epoch);
|
|
324
382
|
} catch (error: unknown) {
|
|
325
383
|
cleanup();
|
|
326
|
-
reject(
|
|
384
|
+
reject(this.#terminalizeFailure(error, epoch));
|
|
327
385
|
}
|
|
328
386
|
|
|
329
387
|
return promise;
|
|
@@ -331,8 +389,9 @@ export class StdioTransport implements MCPTransport {
|
|
|
331
389
|
|
|
332
390
|
async notify(method: string, params?: Record<string, unknown>): Promise<void> {
|
|
333
391
|
const stdin = this.#getStdin();
|
|
392
|
+
const epoch = this.#connectionEpoch;
|
|
334
393
|
if (!this.#connected || !stdin) {
|
|
335
|
-
throw new MCPExpectedFailure();
|
|
394
|
+
throw this.#terminalFailure?.epoch === epoch ? this.#terminalFailure.error : new MCPExpectedFailure();
|
|
336
395
|
}
|
|
337
396
|
|
|
338
397
|
const notification = {
|
|
@@ -343,16 +402,20 @@ export class StdioTransport implements MCPTransport {
|
|
|
343
402
|
|
|
344
403
|
const message = `${JSON.stringify(notification)}\n`;
|
|
345
404
|
try {
|
|
346
|
-
|
|
347
|
-
stdin.write(message);
|
|
348
|
-
stdin.flush();
|
|
405
|
+
await this.#queueWriteFrame(stdin, message, epoch);
|
|
349
406
|
} catch (error) {
|
|
350
|
-
throw
|
|
407
|
+
throw this.#terminalizeFailure(error, epoch);
|
|
351
408
|
}
|
|
352
409
|
}
|
|
353
410
|
|
|
354
411
|
async close(): Promise<void> {
|
|
355
|
-
|
|
412
|
+
const epoch = this.#connectionEpoch;
|
|
413
|
+
let failure = this.#terminalFailure?.epoch === epoch ? this.#terminalFailure.error : undefined;
|
|
414
|
+
if (!failure) {
|
|
415
|
+
failure = new MCPExpectedFailure();
|
|
416
|
+
this.#terminalFailure = { epoch, error: failure };
|
|
417
|
+
}
|
|
418
|
+
await this.#closeInternal(false, failure);
|
|
356
419
|
}
|
|
357
420
|
|
|
358
421
|
#closeInternal(fromReadLoop: boolean, failure?: MCPExpectedFailure): Promise<void> {
|
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
} from "../../session/session-storage";
|
|
26
26
|
import {
|
|
27
27
|
ensureLaunchWorktree,
|
|
28
|
-
|
|
28
|
+
ensureLaunchWorktreeDependencies,
|
|
29
|
+
launchWorktreeCwd,
|
|
29
30
|
planLaunchWorktree,
|
|
30
31
|
type SkcLaunchWorktreePlan,
|
|
31
32
|
} from "../../skc-runtime/launch-worktree";
|
|
@@ -656,7 +657,7 @@ async function validateLiveResumeScope(
|
|
|
656
657
|
: { enabled: true, detached: true, name: null },
|
|
657
658
|
);
|
|
658
659
|
if (!planned.enabled) return fail("invalid_input", "Lifecycle worktree target is invalid.");
|
|
659
|
-
cwd =
|
|
660
|
+
cwd = launchWorktreeCwd(planned);
|
|
660
661
|
} catch (error) {
|
|
661
662
|
return fail(
|
|
662
663
|
"invalid_input",
|
|
@@ -2338,7 +2339,7 @@ function preparePlannedWorktree(plan: SkcLaunchWorktreePlan): SessionLifecycleWo
|
|
|
2338
2339
|
throw new Error("Lifecycle worktree preparation did not preserve the durable worktree identity.");
|
|
2339
2340
|
return {
|
|
2340
2341
|
enabled: true,
|
|
2341
|
-
cwd:
|
|
2342
|
+
cwd: launchWorktreeCwd(prepared),
|
|
2342
2343
|
created: prepared.created,
|
|
2343
2344
|
reused: prepared.reused,
|
|
2344
2345
|
...(prepared.branchName ? { branch: prepared.branchName } : {}),
|
|
@@ -2377,7 +2378,7 @@ async function launchInput(
|
|
|
2377
2378
|
);
|
|
2378
2379
|
if (!planned.enabled) return fail("invalid_input", "Lifecycle worktree target is invalid.");
|
|
2379
2380
|
worktreePlan = planned;
|
|
2380
|
-
cwd =
|
|
2381
|
+
cwd = launchWorktreeCwd(planned);
|
|
2381
2382
|
} catch (error) {
|
|
2382
2383
|
return fail(
|
|
2383
2384
|
"invalid_input",
|
|
@@ -2867,7 +2868,7 @@ async function executeLifecycleResponse(
|
|
|
2867
2868
|
digest: createHash("sha256").update(canonicalJson({ worktree })).digest("hex"),
|
|
2868
2869
|
};
|
|
2869
2870
|
await broker.ledger.transition(identity, "effect_started", { durableEffects });
|
|
2870
|
-
|
|
2871
|
+
ensureLaunchWorktreeDependencies(launch.worktreePlan);
|
|
2871
2872
|
}
|
|
2872
2873
|
} catch (error) {
|
|
2873
2874
|
return fail(
|
|
@@ -40,21 +40,27 @@ import {
|
|
|
40
40
|
resolveSkcTmuxCommand,
|
|
41
41
|
} from "../../skc-runtime/tmux-common";
|
|
42
42
|
import {
|
|
43
|
+
abandonIdentityCreate,
|
|
44
|
+
advanceIdentityCreatePhase,
|
|
43
45
|
captureOwnerGenerationBaseline,
|
|
44
46
|
classifyCgroup,
|
|
45
47
|
isExactScopedBootstrapSuccessReceipt,
|
|
46
48
|
type OwnerGenerationBaseline,
|
|
47
49
|
type OwnerIsolationProbe,
|
|
48
50
|
planTmuxOwnerIsolation,
|
|
51
|
+
releaseIdentityCreate,
|
|
49
52
|
replaceOwnerGeneration,
|
|
53
|
+
reserveIdentityCreate,
|
|
50
54
|
resolveManagedOwnerPredecessorSync,
|
|
51
55
|
type TmuxServerProof,
|
|
52
56
|
} from "../../skc-runtime/tmux-owner-isolation";
|
|
53
57
|
import {
|
|
58
|
+
abandonedIdentityCensus,
|
|
54
59
|
findSkcTmuxSessionByName,
|
|
55
60
|
forceCloseSkcTmuxSession,
|
|
56
61
|
listSkcTmuxSessions,
|
|
57
62
|
type SkcTmuxSessionStatus,
|
|
63
|
+
settleAbandonedIdentity,
|
|
58
64
|
} from "../../skc-runtime/tmux-sessions";
|
|
59
65
|
import type {
|
|
60
66
|
LifecycleErrorReason,
|
|
@@ -873,66 +879,103 @@ export function daemonSpawnCreate(
|
|
|
873
879
|
const { cwd, args } = buildCreateArgv(frame, ids);
|
|
874
880
|
const sessionStateFile = lifecycleRuntimeStateFile(cwd, ids.intendedSessionId, name);
|
|
875
881
|
const stateDir = path.dirname(sessionStateFile);
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
const
|
|
880
|
-
const
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
:
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
882
|
+
// Shared identity create fence, before the final baseline capture and any
|
|
883
|
+
// spawn preparation. The SDK-bus producer is a separate implementation
|
|
884
|
+
// from the notification one and must acquire it in its own right.
|
|
885
|
+
const identityKey = { stateDir, sessionId: ids.intendedSessionId, stateFile: sessionStateFile };
|
|
886
|
+
const reserved = reserveIdentityCreate(identityKey);
|
|
887
|
+
// A dead previous owner that reached the helper may have left an untagged
|
|
888
|
+
// child. Resolve it authority-first instead of treating the row as a
|
|
889
|
+
// permanent collision, which would brick this identity forever.
|
|
890
|
+
const settled = settleAbandonedIdentity(
|
|
891
|
+
reserved,
|
|
892
|
+
identityKey,
|
|
893
|
+
abandonedIdentityCensus(tmux, env, sessionStateFile),
|
|
894
|
+
);
|
|
895
|
+
// Fail closed: an unreachable fence cannot honor the one-create guarantee.
|
|
896
|
+
if (!settled.ok) throw new Error(`skc_lifecycle_identity_create_${settled.code}:${settled.diagnostic}`);
|
|
897
|
+
let reservation = settled.reservation;
|
|
898
|
+
let cleanupUncertain = false;
|
|
899
|
+
try {
|
|
900
|
+
const previousBaseline = await captureOwnerGenerationBaseline(stateDir, ids.intendedSessionId);
|
|
901
|
+
const predecessor = resolveManagedOwnerPredecessorSync(stateDir, ids.intendedSessionId, previousBaseline);
|
|
902
|
+
const generation = crypto.randomUUID();
|
|
903
|
+
const runId = crypto.randomUUID();
|
|
904
|
+
const incarnation = crypto.randomUUID();
|
|
905
|
+
// Detached: no interactive TTY needed (daemon-safe). These values contain
|
|
906
|
+
// only opaque ids and paths needed by the resident sidecar to publish its
|
|
907
|
+
// exact-owner terminal verdict.
|
|
908
|
+
const childEnv: Record<string, string> = {
|
|
909
|
+
SKC_TMUX_LAUNCHED: "1",
|
|
910
|
+
SKC_NOTIFICATIONS: "1",
|
|
911
|
+
SKC_SESSION_ID: ids.intendedSessionId,
|
|
912
|
+
SKC_LIFECYCLE_REQUEST_ID: ids.lifecycleRequestId,
|
|
913
|
+
[SKC_COORDINATOR_SESSION_ID_ENV]: ids.intendedSessionId,
|
|
914
|
+
[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
|
|
915
|
+
[SKC_TMUX_OWNER_GENERATION_ENV]: generation,
|
|
916
|
+
[SKC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
|
|
917
|
+
[SKC_TMUX_OWNER_SERVER_KEY_ENV]: "default",
|
|
918
|
+
SKC_MANAGED_OWNER_COMMAND_JSON: JSON.stringify(["skc", ...args]),
|
|
919
|
+
[MANAGED_OWNER_RUN_ID_ENV]: runId,
|
|
920
|
+
[MANAGED_OWNER_INCARNATION_ENV]: incarnation,
|
|
921
|
+
...(predecessor
|
|
922
|
+
? {
|
|
923
|
+
[MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]: predecessor.predecessorToken,
|
|
924
|
+
[MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: predecessor.generation,
|
|
925
|
+
[MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: predecessor.runId,
|
|
926
|
+
[MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: predecessor.incarnation,
|
|
927
|
+
[MANAGED_OWNER_TRANSCRIPT_PATH_ENV]: env.SKC_SESSION_FILE ?? "",
|
|
928
|
+
}
|
|
929
|
+
: {}),
|
|
930
|
+
};
|
|
931
|
+
if (ids.startupPromptRef) childEnv.SKC_STARTUP_PROMPT_REF = ids.startupPromptRef;
|
|
932
|
+
const envPairs = Object.entries(childEnv)
|
|
933
|
+
.map(([key, value]) => `${key}=${shellQuote(value)}`)
|
|
934
|
+
.join(" ");
|
|
935
|
+
const command = `cd ${shellQuote(cwd)} && exec env ${envPairs} skc ${shellQuote(MANAGED_OWNER_SUPERVISOR_ARG)}`;
|
|
936
|
+
// Record the attempt name BEFORE anything can spawn. Without this the
|
|
937
|
+
// reservation stays at `reserved`, and a successor would read a crash
|
|
938
|
+
// here as "no child can exist" and reclaim an identity that already
|
|
939
|
+
// has an untagged child.
|
|
940
|
+
reservation =
|
|
941
|
+
advanceIdentityCreatePhase(reservation, "helper_invoked", { attemptSessionName: name }) ??
|
|
942
|
+
(() => {
|
|
943
|
+
throw new Error("skc_lifecycle_identity_create_fence_lost");
|
|
944
|
+
})();
|
|
945
|
+
await completeLifecycleSpawnTransaction({
|
|
946
|
+
tmux,
|
|
947
|
+
env,
|
|
948
|
+
sessionId: ids.intendedSessionId,
|
|
949
|
+
generation,
|
|
950
|
+
stateDir,
|
|
951
|
+
cwd,
|
|
952
|
+
sessionName: name,
|
|
953
|
+
sessionStateFile,
|
|
954
|
+
argv: [tmux, "new-session", "-d", "-P", "-F", "#{session_id}", "-s", name, "sh", "-c", command],
|
|
955
|
+
ownerIsolationProbe: opts.ownerIsolationProbe,
|
|
956
|
+
prepareSpawn: () => {
|
|
957
|
+
if (frame.target.kind === "plain_dir") fs.mkdirSync(cwd, { recursive: true });
|
|
958
|
+
},
|
|
959
|
+
previousBaseline,
|
|
960
|
+
});
|
|
928
961
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
962
|
+
return {
|
|
963
|
+
sessionId: ids.intendedSessionId,
|
|
964
|
+
tmuxSession: name,
|
|
965
|
+
sessionStateFile,
|
|
966
|
+
endpointUrl: "",
|
|
967
|
+
topicThreadId: "",
|
|
968
|
+
};
|
|
969
|
+
} catch (error) {
|
|
970
|
+
// `skc_lifecycle_cleanup_uncertain` means a child may survive that we
|
|
971
|
+
// could not remove. Keep the reservation so its evidence outlives us.
|
|
972
|
+
if (error instanceof AggregateError && /cleanup_uncertain/.test(String(error.message)))
|
|
973
|
+
cleanupUncertain = true;
|
|
974
|
+
throw error;
|
|
975
|
+
} finally {
|
|
976
|
+
if (cleanupUncertain) abandonIdentityCreate(reservation);
|
|
977
|
+
else releaseIdentityCreate(reservation);
|
|
978
|
+
}
|
|
936
979
|
};
|
|
937
980
|
}
|
|
938
981
|
|
|
@@ -1086,58 +1129,94 @@ export function daemonResumeSession(
|
|
|
1086
1129
|
const name = tmuxSessionNameFor(resumeId);
|
|
1087
1130
|
const sessionStateFile = lifecycleRuntimeStateFile(resolvedResumeCwd, resumeId, name);
|
|
1088
1131
|
const stateDir = path.dirname(sessionStateFile);
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
:
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
+
// Same shared fence: the cold restart reuses the historical id, so it can
|
|
1133
|
+
// collide with a restore of that exact identity.
|
|
1134
|
+
const identityKey = { stateDir, sessionId: resumeId, stateFile: sessionStateFile };
|
|
1135
|
+
const reserved = reserveIdentityCreate(identityKey);
|
|
1136
|
+
// A dead previous owner that reached the helper may have left an untagged
|
|
1137
|
+
// child. Resolve it authority-first instead of treating the row as a
|
|
1138
|
+
// permanent collision, which would brick this identity forever.
|
|
1139
|
+
const settled = settleAbandonedIdentity(
|
|
1140
|
+
reserved,
|
|
1141
|
+
identityKey,
|
|
1142
|
+
abandonedIdentityCensus(tmux, env, sessionStateFile),
|
|
1143
|
+
);
|
|
1144
|
+
// Fail closed: an unreachable fence cannot honor the one-create guarantee.
|
|
1145
|
+
if (!settled.ok) throw new Error(`skc_lifecycle_identity_create_${settled.code}:${settled.diagnostic}`);
|
|
1146
|
+
let reservation = settled.reservation;
|
|
1147
|
+
let cleanupUncertain = false;
|
|
1148
|
+
try {
|
|
1149
|
+
const previousBaseline = await captureOwnerGenerationBaseline(stateDir, resumeId);
|
|
1150
|
+
const predecessor = resolveManagedOwnerPredecessorSync(stateDir, resumeId, previousBaseline);
|
|
1151
|
+
const generation = crypto.randomUUID();
|
|
1152
|
+
const runId = crypto.randomUUID();
|
|
1153
|
+
const incarnation = crypto.randomUUID();
|
|
1154
|
+
const childEnv: Record<string, string> = {
|
|
1155
|
+
SKC_TMUX_LAUNCHED: "1",
|
|
1156
|
+
SKC_NOTIFICATIONS: "1",
|
|
1157
|
+
[SKC_COORDINATOR_SESSION_ID_ENV]: resumeId,
|
|
1158
|
+
[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
|
|
1159
|
+
[SKC_TMUX_OWNER_GENERATION_ENV]: generation,
|
|
1160
|
+
[SKC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
|
|
1161
|
+
[SKC_TMUX_OWNER_SERVER_KEY_ENV]: "default",
|
|
1162
|
+
SKC_MANAGED_OWNER_COMMAND_JSON: JSON.stringify(["skc", "--resume", resumeId]),
|
|
1163
|
+
[MANAGED_OWNER_RUN_ID_ENV]: runId,
|
|
1164
|
+
[MANAGED_OWNER_INCARNATION_ENV]: incarnation,
|
|
1165
|
+
...(predecessor
|
|
1166
|
+
? {
|
|
1167
|
+
[MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]: predecessor.predecessorToken,
|
|
1168
|
+
[MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: predecessor.generation,
|
|
1169
|
+
[MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: predecessor.runId,
|
|
1170
|
+
[MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: predecessor.incarnation,
|
|
1171
|
+
[MANAGED_OWNER_TRANSCRIPT_PATH_ENV]: env.SKC_SESSION_FILE ?? "",
|
|
1172
|
+
}
|
|
1173
|
+
: {}),
|
|
1174
|
+
};
|
|
1175
|
+
const envPairs = Object.entries(childEnv)
|
|
1176
|
+
.map(([key, value]) => `${key}=${shellQuote(value)}`)
|
|
1177
|
+
.join(" ");
|
|
1178
|
+
const command = `cd ${shellQuote(resolvedResumeCwd)} && exec env ${envPairs} skc ${shellQuote(MANAGED_OWNER_SUPERVISOR_ARG)}`;
|
|
1179
|
+
// Record the attempt name BEFORE anything can spawn. Without this the
|
|
1180
|
+
// reservation stays at `reserved`, and a successor would read a crash
|
|
1181
|
+
// here as "no child can exist" and reclaim an identity that already
|
|
1182
|
+
// has an untagged child.
|
|
1183
|
+
reservation =
|
|
1184
|
+
advanceIdentityCreatePhase(reservation, "helper_invoked", { attemptSessionName: name }) ??
|
|
1185
|
+
(() => {
|
|
1186
|
+
throw new Error("skc_lifecycle_identity_create_fence_lost");
|
|
1187
|
+
})();
|
|
1188
|
+
await completeLifecycleSpawnTransaction({
|
|
1189
|
+
tmux,
|
|
1190
|
+
env,
|
|
1191
|
+
sessionId: resumeId,
|
|
1192
|
+
generation,
|
|
1193
|
+
stateDir,
|
|
1194
|
+
cwd: resolvedResumeCwd,
|
|
1195
|
+
sessionName: name,
|
|
1196
|
+
sessionStateFile,
|
|
1197
|
+
argv: [tmux, "new-session", "-d", "-P", "-F", "#{session_id}", "-s", name, "sh", "-c", command],
|
|
1198
|
+
ownerIsolationProbe: opts.ownerIsolationProbe,
|
|
1199
|
+
previousBaseline,
|
|
1200
|
+
});
|
|
1132
1201
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1202
|
+
return {
|
|
1203
|
+
sessionId: resumeId,
|
|
1204
|
+
tmuxSession: name,
|
|
1205
|
+
sessionStateFile,
|
|
1206
|
+
endpointUrl: "",
|
|
1207
|
+
topicThreadId: "",
|
|
1208
|
+
mode: "cold_restarted",
|
|
1209
|
+
};
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
// `skc_lifecycle_cleanup_uncertain` means a child may survive that we
|
|
1212
|
+
// could not remove. Keep the reservation so its evidence outlives us.
|
|
1213
|
+
if (error instanceof AggregateError && /cleanup_uncertain/.test(String(error.message)))
|
|
1214
|
+
cleanupUncertain = true;
|
|
1215
|
+
throw error;
|
|
1216
|
+
} finally {
|
|
1217
|
+
if (cleanupUncertain) abandonIdentityCreate(reservation);
|
|
1218
|
+
else releaseIdentityCreate(reservation);
|
|
1219
|
+
}
|
|
1141
1220
|
};
|
|
1142
1221
|
}
|
|
1143
1222
|
|
|
@@ -1825,6 +1825,17 @@
|
|
|
1825
1825
|
"packages/coding-agent/test/sdk-operation-inventory.test.ts"
|
|
1826
1826
|
]
|
|
1827
1827
|
},
|
|
1828
|
+
{
|
|
1829
|
+
"sourceId": "slash_command:import-session",
|
|
1830
|
+
"sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
|
|
1831
|
+
"sourceKind": "slash_command",
|
|
1832
|
+
"decision": "exclude",
|
|
1833
|
+
"rationale": "local transcript-file import into a new session; ACP is explicitly disabled and no SDK operation reads caller-selected host paths",
|
|
1834
|
+
"exclusionMetadata": {
|
|
1835
|
+
"adapterMappings": "not_applicable",
|
|
1836
|
+
"testIds": "not_applicable"
|
|
1837
|
+
}
|
|
1838
|
+
},
|
|
1828
1839
|
{
|
|
1829
1840
|
"sourceId": "slash_command:copy",
|
|
1830
1841
|
"sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
|
|
@@ -2870,6 +2881,17 @@
|
|
|
2870
2881
|
"testIds": "not_applicable"
|
|
2871
2882
|
}
|
|
2872
2883
|
},
|
|
2884
|
+
{
|
|
2885
|
+
"sourceId": "agent_session:hasExplicitlyDisabledTools",
|
|
2886
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
2887
|
+
"sourceKind": "agent_session",
|
|
2888
|
+
"decision": "exclude",
|
|
2889
|
+
"rationale": "internal automatic-tool-selection predicate, not a user-facing SDK control seam",
|
|
2890
|
+
"exclusionMetadata": {
|
|
2891
|
+
"adapterMappings": "not_applicable",
|
|
2892
|
+
"testIds": "not_applicable"
|
|
2893
|
+
}
|
|
2894
|
+
},
|
|
2873
2895
|
{
|
|
2874
2896
|
"sourceId": "agent_session:setActiveToolsByName",
|
|
2875
2897
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
@@ -4317,6 +4339,17 @@
|
|
|
4317
4339
|
"testIds": "not_applicable"
|
|
4318
4340
|
}
|
|
4319
4341
|
},
|
|
4342
|
+
{
|
|
4343
|
+
"sourceId": "agent_session:runPrePromptContextCheckForTests",
|
|
4344
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
4345
|
+
"sourceKind": "agent_session",
|
|
4346
|
+
"decision": "exclude",
|
|
4347
|
+
"rationale": "test-only context check seam, not a user-facing SDK control seam",
|
|
4348
|
+
"exclusionMetadata": {
|
|
4349
|
+
"adapterMappings": "not_applicable",
|
|
4350
|
+
"testIds": "not_applicable"
|
|
4351
|
+
}
|
|
4352
|
+
},
|
|
4320
4353
|
{
|
|
4321
4354
|
"sourceId": "agent_session:setRetainedMemorySampler",
|
|
4322
4355
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|