@gajae-code/coding-agent 0.13.0 → 0.13.2
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 +34 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +13 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +87 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/modes/interactive-mode.ts +4 -0
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/process-incarnation.ts +6 -1
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -2
- package/src/sdk/bus/telegram-daemon.ts +151 -41
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/internal/managed-session-storage.ts +136 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/builtin-registry.ts +43 -5
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +5 -4
- package/src/tools/bash.ts +11 -9
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- package/src/web/search/providers/insane.ts +55 -1
|
@@ -28,6 +28,7 @@ export interface ConversationStoreFs {
|
|
|
28
28
|
readFile(file: string, encoding: "utf8"): Promise<string>;
|
|
29
29
|
writeFile(file: string, data: string, options: { mode: number }): Promise<void>;
|
|
30
30
|
rename(from: string, to: string): Promise<void>;
|
|
31
|
+
link(from: string, to: string): Promise<void>;
|
|
31
32
|
unlink(file: string): Promise<void>;
|
|
32
33
|
open(file: string, flags: string): Promise<ConversationStoreFileHandle>;
|
|
33
34
|
stat?(file: string): Promise<{ mtimeMs: number }>;
|
|
@@ -46,11 +47,22 @@ export class ConversationLockTimeoutError extends Error {
|
|
|
46
47
|
interface ConversationStoreLock {
|
|
47
48
|
pid: number;
|
|
48
49
|
incarnation: string;
|
|
50
|
+
isolateId?: string;
|
|
49
51
|
timestamp: number;
|
|
52
|
+
/** Uniquely identifies one acquire; lets a release prove it owns the lock file. */
|
|
53
|
+
nonce?: string;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
const nodeFs: ConversationStoreFs = fs;
|
|
53
|
-
|
|
57
|
+
/** Unique to this module evaluation, including one Bun worker isolate. */
|
|
58
|
+
const processIsolateId = randomUUID();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Lock files this process currently holds or is acquiring, across every store.
|
|
62
|
+
* The token lets a releasing predecessor preserve a successor's marker when
|
|
63
|
+
* the same path has already been reacquired.
|
|
64
|
+
*/
|
|
65
|
+
const heldLockFiles = new Map<string, Set<string>>();
|
|
54
66
|
|
|
55
67
|
export function conversationStorePath(agentDir: string, kind: string, fileName = "conversations.json"): string {
|
|
56
68
|
return path.join(agentDir, "sdk", "daemons", kind, fileName);
|
|
@@ -214,108 +226,220 @@ export class ConversationStore<T extends ConversationRecord> {
|
|
|
214
226
|
const tail = previous.then(() => gate.promise);
|
|
215
227
|
this.#locks.set(this.#file, tail);
|
|
216
228
|
await previous;
|
|
229
|
+
const lockFile = `${this.#file}.lock`;
|
|
217
230
|
let fileLock: ConversationStoreFileHandle | undefined;
|
|
231
|
+
let lockToken: ConversationStoreLock | undefined;
|
|
218
232
|
try {
|
|
219
|
-
|
|
233
|
+
const acquired = await this.#acquireFileLock();
|
|
234
|
+
fileLock = acquired.handle;
|
|
235
|
+
lockToken = acquired.lock;
|
|
220
236
|
return await operation();
|
|
221
237
|
} finally {
|
|
222
238
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
239
|
+
// A failed close must never skip the unlink: that would leak a
|
|
240
|
+
// well-formed lock whose pid is still alive and block every later
|
|
241
|
+
// waiter for the full timeout. The unlink is ownership-checked so
|
|
242
|
+
// it can only ever remove the lock this holder wrote.
|
|
243
|
+
if (fileLock) await fileLock.close().catch(() => undefined);
|
|
244
|
+
if (fileLock && lockToken) await this.#unlinkOwnedLock(lockFile, lockToken);
|
|
228
245
|
} finally {
|
|
246
|
+
if (lockToken) this.#releaseHeldLock(lockFile, lockToken);
|
|
229
247
|
gate.resolve();
|
|
230
248
|
if (this.#locks.get(this.#file) === tail) this.#locks.delete(this.#file);
|
|
231
249
|
}
|
|
232
250
|
}
|
|
233
251
|
}
|
|
234
252
|
|
|
235
|
-
|
|
253
|
+
#releaseHeldLock(lockFile: string, lock: ConversationStoreLock): void {
|
|
254
|
+
const tokens = heldLockFiles.get(lockFile);
|
|
255
|
+
if (!tokens) return;
|
|
256
|
+
tokens.delete(lock.nonce!);
|
|
257
|
+
if (tokens.size === 0) heldLockFiles.delete(lockFile);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async #unlinkOwnedLock(lockFile: string, expected: ConversationStoreLock): Promise<boolean> {
|
|
261
|
+
let parsed: unknown;
|
|
262
|
+
try {
|
|
263
|
+
parsed = JSON.parse(await this.#fs.readFile(lockFile, "utf8"));
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (isMissing(error)) return false;
|
|
266
|
+
return false; // unreadable: never remove a lock we cannot identify
|
|
267
|
+
}
|
|
268
|
+
if (
|
|
269
|
+
!isConversationStoreLock(parsed) ||
|
|
270
|
+
parsed.pid !== expected.pid ||
|
|
271
|
+
parsed.incarnation !== expected.incarnation ||
|
|
272
|
+
parsed.isolateId !== expected.isolateId ||
|
|
273
|
+
parsed.timestamp !== expected.timestamp ||
|
|
274
|
+
parsed.nonce !== expected.nonce
|
|
275
|
+
)
|
|
276
|
+
return false; // a successor holds the path now; leave it
|
|
277
|
+
try {
|
|
278
|
+
await this.#fs.unlink(lockFile);
|
|
279
|
+
return true;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (isMissing(error)) return false;
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async #acquireFileLock(): Promise<{ handle: ConversationStoreFileHandle; lock: ConversationStoreLock }> {
|
|
236
287
|
const lockFile = `${this.#file}.lock`;
|
|
237
288
|
const deadline = Date.now() + this.#lockTimeoutMs;
|
|
238
289
|
await this.#fs.mkdir(this.#directory, { recursive: true, mode: 0o700 });
|
|
239
290
|
for (;;) {
|
|
240
291
|
try {
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
const lock: ConversationStoreLock = {
|
|
244
|
-
pid: this.#pid,
|
|
245
|
-
incarnation: this.#pidIncarnation(this.#pid) ?? "unavailable",
|
|
246
|
-
timestamp: this.#clock(),
|
|
247
|
-
};
|
|
248
|
-
await handle.writeFile(`${JSON.stringify(lock)}\n`, "utf8");
|
|
249
|
-
await handle.sync();
|
|
250
|
-
return handle;
|
|
251
|
-
} catch (error) {
|
|
252
|
-
await handle.close().catch(() => undefined);
|
|
253
|
-
await this.#fs.unlink(lockFile).catch(() => undefined);
|
|
254
|
-
throw error;
|
|
255
|
-
}
|
|
292
|
+
return await this.#createLockFile(lockFile);
|
|
256
293
|
} catch (error) {
|
|
257
294
|
if (!isAlreadyExists(error)) throw error;
|
|
258
295
|
if (await this.#reclaimStaleLock(lockFile)) continue;
|
|
296
|
+
if (await this.#reclaimSameProcessLeak(lockFile)) continue;
|
|
259
297
|
if (Date.now() >= deadline) throw new ConversationLockTimeoutError(lockFile, this.#lockTimeoutMs);
|
|
260
298
|
await this.#sleep(Math.min(10, Math.max(1, deadline - Date.now())));
|
|
261
299
|
}
|
|
262
300
|
}
|
|
263
301
|
}
|
|
264
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Removes a well-formed lock this process wrote but no longer holds. Healing
|
|
305
|
+
* is serialized by its own ownership-tracked reclaim lock, so concurrent
|
|
306
|
+
* waiters cannot delete a successor after the original leak is removed.
|
|
307
|
+
*/
|
|
308
|
+
async #reclaimSameProcessLeak(lockFile: string): Promise<boolean> {
|
|
309
|
+
const expected = await this.#readLock(lockFile);
|
|
310
|
+
if (
|
|
311
|
+
!expected ||
|
|
312
|
+
expected.pid !== this.#pid ||
|
|
313
|
+
expected.isolateId !== processIsolateId ||
|
|
314
|
+
heldLockFiles.has(lockFile)
|
|
315
|
+
)
|
|
316
|
+
return false;
|
|
317
|
+
|
|
318
|
+
const reclaimFile = `${lockFile}.reclaim`;
|
|
319
|
+
const reclaimLock = await this.#acquireReclaimLock(reclaimFile);
|
|
320
|
+
if (!reclaimLock) return false;
|
|
321
|
+
try {
|
|
322
|
+
if (expected.pid !== this.#pid || expected.isolateId !== processIsolateId || heldLockFiles.has(lockFile))
|
|
323
|
+
return false;
|
|
324
|
+
return await this.#unlinkOwnedLock(lockFile, expected);
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
} finally {
|
|
328
|
+
await reclaimLock.handle.close().catch(() => undefined);
|
|
329
|
+
try {
|
|
330
|
+
await this.#unlinkOwnedLock(reclaimFile, reclaimLock.lock).catch(() => undefined);
|
|
331
|
+
} finally {
|
|
332
|
+
this.#releaseHeldLock(reclaimFile, reclaimLock.lock);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
265
337
|
async #reclaimStaleLock(lockFile: string): Promise<boolean> {
|
|
266
338
|
if (!(await this.#isStaleLock(lockFile))) return false;
|
|
267
339
|
const reclaimFile = `${lockFile}.reclaim`;
|
|
268
340
|
const reclaimLock = await this.#acquireReclaimLock(reclaimFile);
|
|
269
341
|
if (!reclaimLock) return false;
|
|
270
342
|
try {
|
|
271
|
-
|
|
272
|
-
await this.#
|
|
273
|
-
return
|
|
343
|
+
const staleLock = await this.#readLock(lockFile);
|
|
344
|
+
if (!staleLock || !(await this.#isStaleLock(lockFile))) return false;
|
|
345
|
+
return await this.#unlinkOwnedLock(lockFile, staleLock);
|
|
346
|
+
} catch {
|
|
347
|
+
return false;
|
|
274
348
|
} finally {
|
|
275
|
-
await reclaimLock.close().catch(() => undefined);
|
|
276
|
-
|
|
349
|
+
await reclaimLock.handle.close().catch(() => undefined);
|
|
350
|
+
try {
|
|
351
|
+
await this.#unlinkOwnedLock(reclaimFile, reclaimLock.lock).catch(() => undefined);
|
|
352
|
+
} finally {
|
|
353
|
+
this.#releaseHeldLock(reclaimFile, reclaimLock.lock);
|
|
354
|
+
}
|
|
277
355
|
}
|
|
278
356
|
}
|
|
279
|
-
|
|
357
|
+
|
|
358
|
+
async #acquireReclaimLock(
|
|
359
|
+
reclaimFile: string,
|
|
360
|
+
): Promise<{ handle: ConversationStoreFileHandle; lock: ConversationStoreLock } | undefined> {
|
|
280
361
|
try {
|
|
281
362
|
return await this.#createLockFile(reclaimFile);
|
|
282
363
|
} catch (error) {
|
|
283
|
-
if (!isAlreadyExists(error)
|
|
284
|
-
await this.#
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
364
|
+
if (!isAlreadyExists(error)) return undefined;
|
|
365
|
+
if (await this.#reclaimStaleLock(reclaimFile)) {
|
|
366
|
+
try {
|
|
367
|
+
return await this.#createLockFile(reclaimFile);
|
|
368
|
+
} catch (retryError) {
|
|
369
|
+
if (isAlreadyExists(retryError)) return undefined;
|
|
370
|
+
throw retryError;
|
|
371
|
+
}
|
|
290
372
|
}
|
|
373
|
+
if (await this.#reclaimSameProcessLeak(reclaimFile)) {
|
|
374
|
+
try {
|
|
375
|
+
return await this.#createLockFile(reclaimFile);
|
|
376
|
+
} catch (retryError) {
|
|
377
|
+
if (isAlreadyExists(retryError)) return undefined;
|
|
378
|
+
throw retryError;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
291
382
|
}
|
|
292
383
|
}
|
|
293
|
-
|
|
294
|
-
|
|
384
|
+
|
|
385
|
+
async #createLockFile(
|
|
386
|
+
lockFile: string,
|
|
387
|
+
): Promise<{ handle: ConversationStoreFileHandle; lock: ConversationStoreLock }> {
|
|
388
|
+
const pendingFile = `${lockFile}.${this.#pid}.${randomUUID()}.pending`;
|
|
389
|
+
const handle = await this.#fs.open(pendingFile, "wx");
|
|
390
|
+
let lock: ConversationStoreLock | undefined;
|
|
391
|
+
let published = false;
|
|
295
392
|
try {
|
|
296
|
-
|
|
393
|
+
lock = {
|
|
297
394
|
pid: this.#pid,
|
|
298
395
|
incarnation: this.#pidIncarnation(this.#pid) ?? "unavailable",
|
|
396
|
+
isolateId: processIsolateId,
|
|
299
397
|
timestamp: this.#clock(),
|
|
398
|
+
nonce: randomUUID(),
|
|
300
399
|
};
|
|
400
|
+
const heldTokens = heldLockFiles.get(lockFile) ?? new Set<string>();
|
|
401
|
+
heldTokens.add(lock.nonce!);
|
|
402
|
+
heldLockFiles.set(lockFile, heldTokens);
|
|
301
403
|
await handle.writeFile(`${JSON.stringify(lock)}\n`, "utf8");
|
|
302
404
|
await handle.sync();
|
|
303
|
-
|
|
405
|
+
await this.#fs.link(pendingFile, lockFile);
|
|
406
|
+
published = true;
|
|
407
|
+
await this.#fs.unlink(pendingFile);
|
|
408
|
+
return { handle, lock };
|
|
304
409
|
} catch (error) {
|
|
305
|
-
|
|
306
|
-
|
|
410
|
+
try {
|
|
411
|
+
await handle.close().catch(() => undefined);
|
|
412
|
+
try {
|
|
413
|
+
if (published && lock) await this.#unlinkOwnedLock(lockFile, lock);
|
|
414
|
+
} finally {
|
|
415
|
+
await this.#fs.unlink(pendingFile).catch(() => undefined);
|
|
416
|
+
}
|
|
417
|
+
} finally {
|
|
418
|
+
if (lock) this.#releaseHeldLock(lockFile, lock);
|
|
419
|
+
}
|
|
307
420
|
throw error;
|
|
308
421
|
}
|
|
309
422
|
}
|
|
423
|
+
|
|
424
|
+
async #readLock(lockFile: string): Promise<ConversationStoreLock | undefined> {
|
|
425
|
+
try {
|
|
426
|
+
const parsed: unknown = JSON.parse(await this.#fs.readFile(lockFile, "utf8"));
|
|
427
|
+
return isConversationStoreLock(parsed) ? parsed : undefined;
|
|
428
|
+
} catch {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
310
433
|
async #isStaleLock(lockFile: string): Promise<boolean> {
|
|
311
434
|
let parsed: unknown;
|
|
312
435
|
try {
|
|
313
436
|
parsed = JSON.parse(await this.#fs.readFile(lockFile, "utf8"));
|
|
314
|
-
} catch
|
|
315
|
-
|
|
316
|
-
|
|
437
|
+
} catch {
|
|
438
|
+
// An unreadable legacy marker may still have an open creator. It is
|
|
439
|
+
// never safe to reclaim it based on age alone.
|
|
440
|
+
return false;
|
|
317
441
|
}
|
|
318
|
-
if (!isConversationStoreLock(parsed)) return
|
|
442
|
+
if (!isConversationStoreLock(parsed)) return false;
|
|
319
443
|
const currentIncarnation = this.#pidIncarnation(parsed.pid);
|
|
320
444
|
return (
|
|
321
445
|
!this.#pidAlive(parsed.pid) ||
|
|
@@ -324,10 +448,6 @@ export class ConversationStore<T extends ConversationRecord> {
|
|
|
324
448
|
(currentIncarnation !== undefined && currentIncarnation !== parsed.incarnation)))
|
|
325
449
|
);
|
|
326
450
|
}
|
|
327
|
-
async #isExpiredUnpublishedLock(lockFile: string): Promise<boolean> {
|
|
328
|
-
const stat = this.#fs.stat ? await this.#fs.stat(lockFile).catch(() => undefined) : undefined;
|
|
329
|
-
return Boolean(stat && this.#clock() - stat.mtimeMs >= UNPUBLISHED_LOCK_STALE_MS);
|
|
330
|
-
}
|
|
331
451
|
|
|
332
452
|
async #readDocument(): Promise<ConversationStoreDocument<T>> {
|
|
333
453
|
try {
|
|
@@ -424,6 +544,7 @@ function isConversationStoreLock(value: unknown): value is ConversationStoreLock
|
|
|
424
544
|
Number.isSafeInteger(value.pid) &&
|
|
425
545
|
value.pid > 0 &&
|
|
426
546
|
typeof value.incarnation === "string" &&
|
|
547
|
+
(value.isolateId === undefined || typeof value.isolateId === "string") &&
|
|
427
548
|
typeof value.timestamp === "number"
|
|
428
549
|
);
|
|
429
550
|
}
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -1168,6 +1168,8 @@ interface SessionRuntime {
|
|
|
1168
1168
|
cursors: CursorRegistry;
|
|
1169
1169
|
/** Current endpoint session identity; never re-key an existing host across a switch. */
|
|
1170
1170
|
id: string;
|
|
1171
|
+
/** Discovery scope is fixed before publication; a live default endpoint is never rotated in place. */
|
|
1172
|
+
endpointScope: "default" | "chat";
|
|
1171
1173
|
idleSeq: number;
|
|
1172
1174
|
/** Interactive asks awaiting a remote answer, by action id. */
|
|
1173
1175
|
pendingInteractive: Map<string, PendingInteractiveAsk>;
|
|
@@ -1179,6 +1181,8 @@ interface SessionRuntime {
|
|
|
1179
1181
|
disposeGateListener: () => void;
|
|
1180
1182
|
/** Whether notification-only delivery and answer resources are active. */
|
|
1181
1183
|
notificationsActive: boolean;
|
|
1184
|
+
/** Provider ownership state is independent from the already-published core SDK runtime. */
|
|
1185
|
+
notificationOwnerState: "ready" | "retry" | "blocked";
|
|
1182
1186
|
/** Rejects new SDK frames while a leased terminal response drains. */
|
|
1183
1187
|
inboundFenced: boolean;
|
|
1184
1188
|
/** Set as soon as terminal teardown is requested, before startup settles. */
|
|
@@ -2289,12 +2293,12 @@ function sdkQuerySurface(
|
|
|
2289
2293
|
}),
|
|
2290
2294
|
configOverrides: ReadonlyMap<string, unknown> = new Map(),
|
|
2291
2295
|
settings: Settings | undefined = undefined,
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
}),
|
|
2296
|
+
turnResultLookup: (selector: {
|
|
2297
|
+
kind: "prompt" | "skill";
|
|
2298
|
+
commandId?: string;
|
|
2299
|
+
turnId?: string;
|
|
2300
|
+
clientRef?: string;
|
|
2301
|
+
}) => unknown = () => ({ status: "unknown" }),
|
|
2298
2302
|
): SessionSurface {
|
|
2299
2303
|
return createSdkSurfaceFactory({
|
|
2300
2304
|
ctx,
|
|
@@ -2304,8 +2308,7 @@ function sdkQuerySurface(
|
|
|
2304
2308
|
getLiveState,
|
|
2305
2309
|
configOverrides,
|
|
2306
2310
|
settings,
|
|
2307
|
-
|
|
2308
|
-
skillStatusLookup,
|
|
2311
|
+
turnResultLookup,
|
|
2309
2312
|
hostTools: () => getInstalledDefinitions("host_tools") !== undefined,
|
|
2310
2313
|
}).query;
|
|
2311
2314
|
}
|
|
@@ -2941,7 +2944,12 @@ function sdkControlSurface(
|
|
|
2941
2944
|
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
2942
2945
|
});
|
|
2943
2946
|
} else if (skillRecon) {
|
|
2944
|
-
void skillRecon.noteTransition(correlation, {
|
|
2947
|
+
void skillRecon.noteTransition(correlation, {
|
|
2948
|
+
type: "agent_end",
|
|
2949
|
+
...(typeof result === "string"
|
|
2950
|
+
? { content: { version: 1, type: "text", text: result, byteLength: 0, truncated: false } }
|
|
2951
|
+
: {}),
|
|
2952
|
+
});
|
|
2945
2953
|
}
|
|
2946
2954
|
return result;
|
|
2947
2955
|
},
|
|
@@ -3655,6 +3663,24 @@ export function createNotificationsExtension(
|
|
|
3655
3663
|
await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
|
|
3656
3664
|
return "ready";
|
|
3657
3665
|
}
|
|
3666
|
+
|
|
3667
|
+
function retainNotificationRootRegistration(
|
|
3668
|
+
runtime: SessionRuntime,
|
|
3669
|
+
settings: Settings,
|
|
3670
|
+
cwd: string,
|
|
3671
|
+
id: string,
|
|
3672
|
+
registrationToken: string,
|
|
3673
|
+
): void {
|
|
3674
|
+
if (runtimes.get(id) === runtime && !runtime.stopping) {
|
|
3675
|
+
runtime.notificationRootRegistration = { settings, cwd, registrationToken };
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
runtime.notificationRootRegistration = { settings, cwd, registrationToken };
|
|
3679
|
+
cleanupRetries.set(id, runtime);
|
|
3680
|
+
void stopSession(id, "session", runtime).catch(error =>
|
|
3681
|
+
logger.warn(`notifications: late Telegram root unregister failed: ${String(error)}`),
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3658
3684
|
const identityControlOperations = new Set([
|
|
3659
3685
|
"session.new",
|
|
3660
3686
|
"session.fork",
|
|
@@ -4150,8 +4176,6 @@ export function createNotificationsExtension(
|
|
|
4150
4176
|
) => {
|
|
4151
4177
|
await kindReconciliation.noteAccepted("prompt", correlation, clientRef);
|
|
4152
4178
|
};
|
|
4153
|
-
const lookupPromptStatus = (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
4154
|
-
kindReconciliation.lookup("prompt", selector);
|
|
4155
4179
|
const releasePromptAdmission = (clientRef?: string) => kindReconciliation.releaseAdmission("prompt", clientRef);
|
|
4156
4180
|
const removePendingPromptCorrelation = (correlation: { commandId: string; turnId: string }) => {
|
|
4157
4181
|
const pendingIndex = pendingPromptCorrelations.findIndex(
|
|
@@ -4469,7 +4493,7 @@ export function createNotificationsExtension(
|
|
|
4469
4493
|
}
|
|
4470
4494
|
}
|
|
4471
4495
|
try {
|
|
4472
|
-
await kindReconciliation.finalizePromptOutcome(correlation, winner, extra?.error);
|
|
4496
|
+
await kindReconciliation.finalizePromptOutcome(correlation, winner, extra?.error, extra?.finalText);
|
|
4473
4497
|
} catch (error) {
|
|
4474
4498
|
// The durable pending claim survives; publishing an unpersisted terminal
|
|
4475
4499
|
// would contradict it, so fail the endpoint closed instead.
|
|
@@ -4587,8 +4611,7 @@ export function createNotificationsExtension(
|
|
|
4587
4611
|
},
|
|
4588
4612
|
configOverrides,
|
|
4589
4613
|
settings,
|
|
4590
|
-
|
|
4591
|
-
selector => kindReconciliation.lookup("skill", selector),
|
|
4614
|
+
selector => kindReconciliation.lookupResult(selector.kind, selector),
|
|
4592
4615
|
),
|
|
4593
4616
|
id,
|
|
4594
4617
|
revisions,
|
|
@@ -4943,6 +4966,7 @@ export function createNotificationsExtension(
|
|
|
4943
4966
|
revisions,
|
|
4944
4967
|
cursors,
|
|
4945
4968
|
id,
|
|
4969
|
+
endpointScope: isolateChatEndpoint ? "chat" : "default",
|
|
4946
4970
|
idleSeq: 0,
|
|
4947
4971
|
pendingInteractive,
|
|
4948
4972
|
brokerRegistrationActive: false,
|
|
@@ -4953,6 +4977,7 @@ export function createNotificationsExtension(
|
|
|
4953
4977
|
disposeFileSink: () => {},
|
|
4954
4978
|
disposeGateListener: () => {},
|
|
4955
4979
|
notificationsActive: false,
|
|
4980
|
+
notificationOwnerState: "ready",
|
|
4956
4981
|
enableNotifications: () => {},
|
|
4957
4982
|
disposeGateTerminalController: () => {},
|
|
4958
4983
|
disposeAckRecoveryParticipant: () => {},
|
|
@@ -5549,48 +5574,46 @@ export function createNotificationsExtension(
|
|
|
5549
5574
|
await cleanupAbandonedStartup();
|
|
5550
5575
|
return { status: "failed" };
|
|
5551
5576
|
}
|
|
5577
|
+
// Record canonical identity before transport startup so lifecycle events emitted
|
|
5578
|
+
// by an early server-start callback cannot overtake it in replay. The direct
|
|
5579
|
+
// socket publication remains below, after the server is listening.
|
|
5580
|
+
const identityHeader = {
|
|
5581
|
+
type: "identity_header",
|
|
5582
|
+
sessionId: id,
|
|
5583
|
+
...buildIdentity(ctx.cwd, ctx.sessionManager.getSessionName()),
|
|
5584
|
+
};
|
|
5585
|
+
host.emitEvent({ kind: identityHeader.type, payload: identityHeader });
|
|
5586
|
+
// Core SDK authority is published before optional notification adapters acquire
|
|
5587
|
+
// daemon ownership. A blocked adapter must not make an interactive session
|
|
5588
|
+
// undiscoverable or uncontrollable.
|
|
5589
|
+
const endpoint = await sdkRuntime.startTransport();
|
|
5590
|
+
initializedRuntime.notificationOwnerState = "ready";
|
|
5552
5591
|
if (notificationsEnabledForSession && settingsAvailable && settings) {
|
|
5592
|
+
initializedRuntime.notificationOwnerState = "retry";
|
|
5553
5593
|
try {
|
|
5554
|
-
let registrationToken: string | undefined;
|
|
5555
5594
|
const ownership = await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id, token => {
|
|
5556
|
-
|
|
5595
|
+
retainNotificationRootRegistration(initializedRuntime, settings, ctx.cwd, id, token);
|
|
5557
5596
|
});
|
|
5597
|
+
initializedRuntime.notificationOwnerState =
|
|
5598
|
+
ownership === "ready" || (ownership === "blocked_identity_with_sibling" && isolateChatEndpoint)
|
|
5599
|
+
? "ready"
|
|
5600
|
+
: "blocked";
|
|
5558
5601
|
if (ownership === "blocked_identity") {
|
|
5559
|
-
|
|
5560
|
-
finishStartup(result);
|
|
5561
|
-
await cleanupAbandonedStartup();
|
|
5562
|
-
return result;
|
|
5602
|
+
logger.warn("notifications: Telegram daemon ownership is blocked; core SDK remains available.");
|
|
5563
5603
|
}
|
|
5564
5604
|
if (ownership === "blocked_identity_with_sibling" && !isolateChatEndpoint) {
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
const result = await startSession(ctx);
|
|
5569
|
-
finishStartup(result);
|
|
5570
|
-
return result;
|
|
5571
|
-
}
|
|
5572
|
-
if (registrationToken !== undefined) {
|
|
5573
|
-
runtime.notificationRootRegistration = { settings, cwd: ctx.cwd, registrationToken };
|
|
5605
|
+
logger.warn(
|
|
5606
|
+
"notifications: Telegram ownership changed after core publication; preserving the canonical endpoint and withholding adapters.",
|
|
5607
|
+
);
|
|
5574
5608
|
}
|
|
5575
5609
|
} catch (error) {
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5610
|
+
initializedRuntime.notificationOwnerState = "retry";
|
|
5611
|
+
logger.warn(
|
|
5612
|
+
`notifications: provider daemon ownership unavailable; core SDK remains available: ${String(error)}`,
|
|
5613
|
+
);
|
|
5580
5614
|
}
|
|
5581
5615
|
}
|
|
5582
5616
|
|
|
5583
|
-
// Startup contract: configured notification daemon ownership must be ready
|
|
5584
|
-
// before identity or endpoint publication. Native frames are ephemeral, so
|
|
5585
|
-
// publish identity only after readiness; late SDK consumers recover it from
|
|
5586
|
-
// event_replay.
|
|
5587
|
-
const identityHeader = {
|
|
5588
|
-
type: "identity_header",
|
|
5589
|
-
sessionId: id,
|
|
5590
|
-
...buildIdentity(ctx.cwd, ctx.sessionManager.getSessionName()),
|
|
5591
|
-
};
|
|
5592
|
-
host.emitEvent({ kind: identityHeader.type, payload: identityHeader });
|
|
5593
|
-
const endpoint = await sdkRuntime.startTransport();
|
|
5594
5617
|
// The native server owns the only authoritative view of this host's live
|
|
5595
5618
|
// SDK client sockets; publish it so a detached session host can bound its
|
|
5596
5619
|
// own lifetime without probing the OS (#4010). The handle is this
|
|
@@ -5849,7 +5872,10 @@ export function createNotificationsExtension(
|
|
|
5849
5872
|
}
|
|
5850
5873
|
|
|
5851
5874
|
const sessionRuntime: NotificationSessionRuntime<ExtensionContext> = {
|
|
5852
|
-
isRunning: binding =>
|
|
5875
|
+
isRunning: binding => {
|
|
5876
|
+
const runtime = runtimes.get(binding.sessionId);
|
|
5877
|
+
return runtime?.notificationsActive === true && runtime.notificationOwnerState === "ready";
|
|
5878
|
+
},
|
|
5853
5879
|
start: async binding => {
|
|
5854
5880
|
if (sessionStartPromises.has(binding.sessionId)) {
|
|
5855
5881
|
const result = await startSession(binding.context);
|
|
@@ -5862,6 +5888,28 @@ export function createNotificationsExtension(
|
|
|
5862
5888
|
}
|
|
5863
5889
|
const runtime = runtimes.get(binding.sessionId);
|
|
5864
5890
|
if (runtime) {
|
|
5891
|
+
if (runtime.notificationOwnerState === "retry") {
|
|
5892
|
+
const { cfg, settings, settingsAvailable } = resolveSettings(options.settings);
|
|
5893
|
+
if (!settingsAvailable || !settings) return "failed";
|
|
5894
|
+
try {
|
|
5895
|
+
const ownership = await ensureConfiguredDaemonOwners(
|
|
5896
|
+
settings,
|
|
5897
|
+
cfg,
|
|
5898
|
+
binding.cwd,
|
|
5899
|
+
binding.sessionId,
|
|
5900
|
+
token => {
|
|
5901
|
+
retainNotificationRootRegistration(runtime, settings, binding.cwd, binding.sessionId, token);
|
|
5902
|
+
},
|
|
5903
|
+
);
|
|
5904
|
+
runtime.notificationOwnerState =
|
|
5905
|
+
ownership === "ready" ||
|
|
5906
|
+
(ownership === "blocked_identity_with_sibling" && runtime.endpointScope === "chat")
|
|
5907
|
+
? "ready"
|
|
5908
|
+
: "blocked";
|
|
5909
|
+
} catch {
|
|
5910
|
+
return "failed";
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5865
5913
|
return "started";
|
|
5866
5914
|
}
|
|
5867
5915
|
const result = await startSession(binding.context);
|
|
@@ -5871,8 +5919,11 @@ export function createNotificationsExtension(
|
|
|
5871
5919
|
isolateTelegram: async binding => {
|
|
5872
5920
|
const runtime = runtimes.get(binding.sessionId);
|
|
5873
5921
|
if (runtime) {
|
|
5874
|
-
|
|
5875
|
-
|
|
5922
|
+
if (runtime.endpointScope === "default") {
|
|
5923
|
+
runtime.notificationOwnerState = "blocked";
|
|
5924
|
+
return "failed";
|
|
5925
|
+
}
|
|
5926
|
+
return "started";
|
|
5876
5927
|
}
|
|
5877
5928
|
forceIsolatedChatSessions.add(binding.sessionId);
|
|
5878
5929
|
const result = await startSession(binding.context);
|
|
@@ -5916,6 +5967,7 @@ export function createNotificationsExtension(
|
|
|
5916
5967
|
// Activation is only valid after the controller commits a stable policy;
|
|
5917
5968
|
// never expose deferred presentations while provisional policy is held.
|
|
5918
5969
|
if (runtime.policySuspended) return;
|
|
5970
|
+
if (runtime.notificationOwnerState !== "ready") return;
|
|
5919
5971
|
runtime.enableNotifications();
|
|
5920
5972
|
runtime.gatePresentations?.setPublicationSuspended(false);
|
|
5921
5973
|
runtime.gatePresentations?.activateDeferred(runtime.workflowGatePublicationEpoch);
|
|
@@ -5942,6 +5994,8 @@ export function createNotificationsExtension(
|
|
|
5942
5994
|
registrationToken,
|
|
5943
5995
|
});
|
|
5944
5996
|
}
|
|
5997
|
+
if (runtime?.notificationOwnerState === "blocked")
|
|
5998
|
+
runtime.notificationOwnerState = result === "ready" ? "ready" : "blocked";
|
|
5945
5999
|
return result;
|
|
5946
6000
|
} catch {
|
|
5947
6001
|
return "failed";
|