@gajae-code/coding-agent 0.16.0 → 0.16.3
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 +52 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +15 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +12 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +156 -32
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +34 -19
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +6 -1
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +75 -7
- package/src/sdk/cli/session-cli.ts +68 -35
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +26 -2
- package/src/sdk/host/host.ts +6 -1
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +497 -31
- package/src/sdk/host/session-runtime.ts +387 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +210 -94
- package/src/sdk/session.ts +59 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +158 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +4 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +86 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { logger } from "@gajae-code/utils";
|
|
2
3
|
import { SdkClientError } from "../client/client";
|
|
3
4
|
import { SESSION_PREPARED_EVENT } from "../host/host";
|
|
4
5
|
|
|
@@ -260,6 +261,9 @@ export class ChatDaemonRuntime {
|
|
|
260
261
|
readonly #cleanupWork = new Map<string, Promise<void>>();
|
|
261
262
|
readonly #retirementWork = new Map<string, Promise<void>>();
|
|
262
263
|
readonly #attachmentBarriers = new Map<string, Promise<void>>();
|
|
264
|
+
readonly #cleanupFailures = new Map<string, unknown>();
|
|
265
|
+
#replayReady: Promise<void> = Promise.resolve();
|
|
266
|
+
#stopping = false;
|
|
263
267
|
#discord: DiscordNotificationDaemon | undefined;
|
|
264
268
|
#slack: SlackNotificationDaemon | undefined;
|
|
265
269
|
#presentation: NotificationPresentationEngine | undefined;
|
|
@@ -276,24 +280,40 @@ export class ChatDaemonRuntime {
|
|
|
276
280
|
...deps.routerDeps,
|
|
277
281
|
onFrame: async (attachment, frame) => await this.#handleFrame(attachment, frame),
|
|
278
282
|
onAttachment: async attachment => this.#onAttachment(attachment),
|
|
283
|
+
onReplayReady: async () => await this.#replayReady,
|
|
279
284
|
onSessionRemoved: async (attachment, reason) => await this.#onSessionRemoved(attachment, reason),
|
|
280
285
|
},
|
|
281
286
|
});
|
|
282
287
|
}
|
|
283
288
|
|
|
284
289
|
async start(): Promise<void> {
|
|
290
|
+
const replayReady = Promise.withResolvers<void>();
|
|
291
|
+
this.#replayReady = replayReady.promise;
|
|
292
|
+
// Startup can fail before any attachment observes the replay barrier. Mark
|
|
293
|
+
// the rejection handled without changing what actual waiters observe.
|
|
294
|
+
void this.#replayReady.catch(() => undefined);
|
|
285
295
|
if (this.#cleanupWork.size > 0 || this.#retirementWork.size > 0) {
|
|
286
296
|
const retirements = [...this.#retirementWork.entries()];
|
|
297
|
+
const cleanup = [...this.#cleanupWork.values()];
|
|
298
|
+
const retirement = retirements.map(([, work]) => work);
|
|
287
299
|
const settled = await Promise.race([
|
|
288
|
-
Promise.all([
|
|
289
|
-
Promise.allSettled([...this.#cleanupWork.values()]),
|
|
290
|
-
Promise.all(retirements.map(([, retirement]) => retirement)),
|
|
291
|
-
]).then(() => true),
|
|
300
|
+
Promise.all([Promise.allSettled(cleanup), Promise.allSettled(retirement)]).then(() => true),
|
|
292
301
|
Bun.sleep(5_000).then(() => false),
|
|
293
302
|
]);
|
|
294
303
|
if (!settled) throw new Error("Prior provider cleanup did not settle before chat daemon restart.");
|
|
295
|
-
|
|
296
|
-
|
|
304
|
+
const failures = await Promise.all([Promise.allSettled(cleanup), Promise.allSettled(retirement)]);
|
|
305
|
+
const rejected = failures
|
|
306
|
+
.flat()
|
|
307
|
+
.filter((result): result is PromiseRejectedResult => result.status === "rejected");
|
|
308
|
+
if (rejected.length > 0) {
|
|
309
|
+
logger.warn(`SDK chat daemon retained ${rejected.length} prior cleanup failure(s) for recovery.`);
|
|
310
|
+
}
|
|
311
|
+
for (const [sessionId, work] of retirements)
|
|
312
|
+
if (this.#retirementWork.get(sessionId) === work && !rejected.some(result => result.status === "rejected"))
|
|
313
|
+
this.#retirementWork.delete(sessionId);
|
|
314
|
+
for (const [sessionId, work] of [...this.#cleanupWork.entries()])
|
|
315
|
+
if (work !== undefined && !rejected.some(result => result.status === "rejected"))
|
|
316
|
+
this.#cleanupWork.delete(sessionId);
|
|
297
317
|
}
|
|
298
318
|
this.#attachments.clear();
|
|
299
319
|
const retainedProvider =
|
|
@@ -308,8 +328,10 @@ export class ChatDaemonRuntime {
|
|
|
308
328
|
try {
|
|
309
329
|
await this.#router.start();
|
|
310
330
|
await retainedProvider.start();
|
|
331
|
+
replayReady.resolve();
|
|
311
332
|
return;
|
|
312
333
|
} catch (error) {
|
|
334
|
+
replayReady.reject(error);
|
|
313
335
|
await this.#router.stop().catch(() => undefined);
|
|
314
336
|
throw error;
|
|
315
337
|
}
|
|
@@ -370,7 +392,9 @@ export class ChatDaemonRuntime {
|
|
|
370
392
|
await this.#router.start();
|
|
371
393
|
if (this.#discord) await this.#discord.start();
|
|
372
394
|
if (this.#slack) await this.#slack.start();
|
|
395
|
+
replayReady.resolve();
|
|
373
396
|
} catch (error) {
|
|
397
|
+
replayReady.reject(error);
|
|
374
398
|
await this.stop();
|
|
375
399
|
throw error;
|
|
376
400
|
}
|
|
@@ -386,6 +410,20 @@ export class ChatDaemonRuntime {
|
|
|
386
410
|
}
|
|
387
411
|
|
|
388
412
|
async stop(): Promise<void> {
|
|
413
|
+
this.#stopping = true;
|
|
414
|
+
let routerError: unknown;
|
|
415
|
+
try {
|
|
416
|
+
await this.#router.stop();
|
|
417
|
+
} catch (error) {
|
|
418
|
+
routerError = error;
|
|
419
|
+
}
|
|
420
|
+
const cleanupDrain = Promise.allSettled([...this.#cleanupWork.values(), ...this.#retirementWork.values()]);
|
|
421
|
+
const cleanupSettled = await Promise.race([cleanupDrain.then(() => true), Bun.sleep(5_000).then(() => false)]);
|
|
422
|
+
const cleanupResults = cleanupSettled
|
|
423
|
+
? await cleanupDrain
|
|
424
|
+
: [{ status: "rejected" as const, reason: new Error("Provider cleanup exceeded shutdown drain.") }];
|
|
425
|
+
if (!cleanupSettled)
|
|
426
|
+
logger.warn("SDK chat daemon cleanup exceeded the shutdown drain; provider stop will invalidate it.");
|
|
389
427
|
const providerResults = await Promise.allSettled([this.#discord?.stop(), this.#slack?.stop()]);
|
|
390
428
|
const discordRestartBlocked = this.#discord?.restartBlocked() ?? false;
|
|
391
429
|
const slackRestartBlocked = this.#slack?.restartBlocked() ?? false;
|
|
@@ -395,10 +433,12 @@ export class ChatDaemonRuntime {
|
|
|
395
433
|
this.#presentation = undefined;
|
|
396
434
|
this.#transportHealthy = undefined;
|
|
397
435
|
}
|
|
398
|
-
|
|
399
|
-
const failures =
|
|
400
|
-
(
|
|
401
|
-
|
|
436
|
+
this.#stopping = false;
|
|
437
|
+
const failures = [
|
|
438
|
+
...(routerError === undefined ? [] : [{ status: "rejected" as const, reason: routerError }]),
|
|
439
|
+
...cleanupResults,
|
|
440
|
+
...providerResults,
|
|
441
|
+
].filter((result): result is PromiseRejectedResult => result.status === "rejected");
|
|
402
442
|
if (failures.length > 0)
|
|
403
443
|
throw new AggregateError(
|
|
404
444
|
failures.map(result => result.reason),
|
|
@@ -431,19 +471,45 @@ export class ChatDaemonRuntime {
|
|
|
431
471
|
}
|
|
432
472
|
|
|
433
473
|
async #onAttachment(attachment: SessionAttachment): Promise<void> {
|
|
474
|
+
const revivedTransport = this.#attachments.get(attachment.sessionId) === attachment;
|
|
434
475
|
this.#attachments.set(attachment.sessionId, attachment);
|
|
476
|
+
if (revivedTransport) {
|
|
477
|
+
this.#presentation?.connectSession(attachment.sessionId, {
|
|
478
|
+
sendReply: route => attachment.send({ type: "reply", id: route.actionId, answer: route.answer }),
|
|
479
|
+
});
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
435
482
|
const discord = this.#discord;
|
|
436
483
|
const slack = this.#slack;
|
|
437
484
|
const barrier = (async () => {
|
|
438
485
|
const retirement = this.#retirementWork.get(attachment.sessionId);
|
|
486
|
+
let retirementFailure: unknown;
|
|
439
487
|
if (retirement) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
488
|
+
try {
|
|
489
|
+
await retirement;
|
|
490
|
+
} catch (error) {
|
|
491
|
+
retirementFailure = error;
|
|
492
|
+
logger.warn(`SDK chat daemon retirement cleanup failed before successor attachment: ${String(error)}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const cleanup = this.#cleanupWork.get(attachment.sessionId);
|
|
496
|
+
if (cleanup) await cleanup.catch(error => logger.warn(`SDK cleanup retry pending: ${String(error)}`));
|
|
497
|
+
const [discordRecovered, slackRecovered] = await Promise.all([
|
|
498
|
+
discord?.recoverCleanup(attachment.sessionId, attachment.generation, attachment.authorityId) ?? true,
|
|
499
|
+
slack?.recoverCleanup(attachment.sessionId, attachment.generation, attachment.authorityId) ?? true,
|
|
500
|
+
]);
|
|
501
|
+
if (!discordRecovered || !slackRecovered)
|
|
502
|
+
throw new Error("successor_attachment_blocked_until_retirement_is_verified");
|
|
503
|
+
if (cleanup !== undefined && this.#cleanupWork.get(attachment.sessionId) === cleanup) {
|
|
504
|
+
this.#cleanupWork.delete(attachment.sessionId);
|
|
505
|
+
this.#cleanupFailures.delete(attachment.sessionId);
|
|
443
506
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
507
|
+
if (
|
|
508
|
+
retirementFailure !== undefined &&
|
|
509
|
+
retirement &&
|
|
510
|
+
this.#retirementWork.get(attachment.sessionId) === retirement
|
|
511
|
+
)
|
|
512
|
+
this.#retirementWork.delete(attachment.sessionId);
|
|
447
513
|
})();
|
|
448
514
|
this.#attachmentBarriers.set(attachment.sessionId, barrier);
|
|
449
515
|
try {
|
|
@@ -470,7 +536,7 @@ export class ChatDaemonRuntime {
|
|
|
470
536
|
return;
|
|
471
537
|
}
|
|
472
538
|
this.#attachments.delete(attachment.sessionId);
|
|
473
|
-
if (reason === "removed") await this.#trackCleanup(attachment);
|
|
539
|
+
if (reason === "removed" && !this.#stopping) await this.#trackCleanup(attachment);
|
|
474
540
|
}
|
|
475
541
|
|
|
476
542
|
#trackRetirement(attachment: SessionAttachment): Promise<void> {
|
|
@@ -478,7 +544,13 @@ export class ChatDaemonRuntime {
|
|
|
478
544
|
const discord = this.#discord;
|
|
479
545
|
const slack = this.#slack;
|
|
480
546
|
const previous = this.#retirementWork.get(sessionId);
|
|
481
|
-
const work = (
|
|
547
|
+
const work = (
|
|
548
|
+
previous
|
|
549
|
+
? previous.catch(error => {
|
|
550
|
+
this.#cleanupFailures.set(sessionId, error);
|
|
551
|
+
})
|
|
552
|
+
: Promise.resolve()
|
|
553
|
+
).then(async () => {
|
|
482
554
|
// Capture the predecessor providers before stop() can clear the runtime
|
|
483
555
|
// fields. The successor barrier owns this promise until every retirement
|
|
484
556
|
// waiter settles, even when takeover overlaps worker shutdown.
|
|
@@ -486,6 +558,12 @@ export class ChatDaemonRuntime {
|
|
|
486
558
|
await slack?.retireAttachment(sessionId, attachment.generation);
|
|
487
559
|
});
|
|
488
560
|
this.#retirementWork.set(sessionId, work);
|
|
561
|
+
void work.then(
|
|
562
|
+
() => {
|
|
563
|
+
if (this.#retirementWork.get(sessionId) === work) this.#retirementWork.delete(sessionId);
|
|
564
|
+
},
|
|
565
|
+
() => undefined,
|
|
566
|
+
);
|
|
489
567
|
return work;
|
|
490
568
|
}
|
|
491
569
|
|
|
@@ -507,7 +585,10 @@ export class ChatDaemonRuntime {
|
|
|
507
585
|
}
|
|
508
586
|
if (name === SESSION_PREPARED_EVENT || bodyType === SESSION_PREPARED_EVENT) return;
|
|
509
587
|
if (name === "session_ready") {
|
|
510
|
-
|
|
588
|
+
// The attachment itself is the endpoint-generation authority. An
|
|
589
|
+
// unpositioned session_ready frame deliberately carries no ring
|
|
590
|
+
// generation; reject only an explicit, conflicting Router-owned value.
|
|
591
|
+
if (correlated.generation !== undefined && correlated.generation !== attachment.generation) return;
|
|
511
592
|
await this.#resume(
|
|
512
593
|
sessionId,
|
|
513
594
|
attachment.generation,
|
|
@@ -550,7 +631,7 @@ export class ChatDaemonRuntime {
|
|
|
550
631
|
await this.#slack.notify(
|
|
551
632
|
sessionId,
|
|
552
633
|
content,
|
|
553
|
-
notification.type === "action_needed" ? notification.id : undefined,
|
|
634
|
+
notification.type === "action_needed" && notification.kind === "ask" ? notification.id : undefined,
|
|
554
635
|
attachment.generation,
|
|
555
636
|
publicationId,
|
|
556
637
|
);
|
|
@@ -559,13 +640,26 @@ export class ChatDaemonRuntime {
|
|
|
559
640
|
#trackCleanup(attachment: SessionAttachment): Promise<void> {
|
|
560
641
|
const sessionId = attachment.sessionId;
|
|
561
642
|
const previous = this.#cleanupWork.get(sessionId);
|
|
562
|
-
const work = (
|
|
643
|
+
const work = (
|
|
644
|
+
previous
|
|
645
|
+
? previous.catch(error => {
|
|
646
|
+
this.#cleanupFailures.set(sessionId, error);
|
|
647
|
+
})
|
|
648
|
+
: Promise.resolve()
|
|
649
|
+
).then(async () => {
|
|
563
650
|
await this.#closeProviders(attachment);
|
|
564
651
|
});
|
|
565
652
|
this.#cleanupWork.set(sessionId, work);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
653
|
+
void work.then(
|
|
654
|
+
() => {
|
|
655
|
+
if (this.#cleanupWork.get(sessionId) === work) {
|
|
656
|
+
this.#cleanupWork.delete(sessionId);
|
|
657
|
+
this.#cleanupFailures.delete(sessionId);
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
() => undefined,
|
|
661
|
+
);
|
|
662
|
+
return work;
|
|
569
663
|
}
|
|
570
664
|
async #closeProviders(attachment: SessionAttachment): Promise<void> {
|
|
571
665
|
const discord = this.#discord;
|
|
@@ -166,8 +166,9 @@ export class DiscordNotificationDaemon {
|
|
|
166
166
|
expectedGeneration?: number,
|
|
167
167
|
) => SessionAttachment | null | Promise<SessionAttachment | null>;
|
|
168
168
|
readonly #effects: ChatEffectJournal;
|
|
169
|
-
readonly #activeWork = new
|
|
169
|
+
readonly #activeWork = new Map<Promise<unknown>, string | undefined>();
|
|
170
170
|
readonly #workInvalidators = new Set<() => Promise<void>>();
|
|
171
|
+
readonly #sessionInvalidators = new Map<string, Set<() => Promise<void>>>();
|
|
171
172
|
readonly #inflightInbound = new Set<string>();
|
|
172
173
|
#started = false;
|
|
173
174
|
#lifecycleGeneration = 0;
|
|
@@ -264,7 +265,8 @@ export class DiscordNotificationDaemon {
|
|
|
264
265
|
await this.options.provider.start(
|
|
265
266
|
async event => {
|
|
266
267
|
if (lifecycleGeneration !== this.#lifecycleGeneration || !this.#started) return;
|
|
267
|
-
await this.#
|
|
268
|
+
const record = await this.#byThread(event.guildId, event.parentId, event.threadId);
|
|
269
|
+
await this.#track(this.handleInbound(event), record?.sessionId);
|
|
268
270
|
},
|
|
269
271
|
() => {},
|
|
270
272
|
);
|
|
@@ -375,6 +377,9 @@ export class DiscordNotificationDaemon {
|
|
|
375
377
|
this.#workGeneration += 1;
|
|
376
378
|
await Promise.allSettled([...this.#workInvalidators].map(invalidate => invalidate()));
|
|
377
379
|
}
|
|
380
|
+
async #invalidateSessionWork(sessionId: string): Promise<void> {
|
|
381
|
+
await Promise.allSettled([...(this.#sessionInvalidators.get(sessionId) ?? [])].map(invalidate => invalidate()));
|
|
382
|
+
}
|
|
378
383
|
|
|
379
384
|
async #drainActiveWork(deadline: number): Promise<void> {
|
|
380
385
|
while (this.#activeWork.size > 0) {
|
|
@@ -385,7 +390,7 @@ export class DiscordNotificationDaemon {
|
|
|
385
390
|
return;
|
|
386
391
|
}
|
|
387
392
|
const settled = await Promise.race([
|
|
388
|
-
Promise.allSettled([...this.#activeWork]).then(() => true),
|
|
393
|
+
Promise.allSettled([...this.#activeWork.keys()]).then(() => true),
|
|
389
394
|
Bun.sleep(remaining).then(() => false),
|
|
390
395
|
]);
|
|
391
396
|
if (!settled) {
|
|
@@ -395,6 +400,17 @@ export class DiscordNotificationDaemon {
|
|
|
395
400
|
}
|
|
396
401
|
}
|
|
397
402
|
}
|
|
403
|
+
async #drainSessionWork(sessionId: string, deadline: number): Promise<void> {
|
|
404
|
+
while (true) {
|
|
405
|
+
const work = [...this.#activeWork.entries()]
|
|
406
|
+
.filter(([, activeSessionId]) => activeSessionId === sessionId)
|
|
407
|
+
.map(([promise]) => promise);
|
|
408
|
+
if (work.length === 0) return;
|
|
409
|
+
const remaining = deadline - this.#now();
|
|
410
|
+
if (remaining <= 0) return;
|
|
411
|
+
await Promise.race([Promise.allSettled(work), Bun.sleep(remaining)]);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
398
414
|
|
|
399
415
|
#recordProviderLifecycleError(error: unknown): void {
|
|
400
416
|
this.#providerLifecycleError = error;
|
|
@@ -422,7 +438,7 @@ export class DiscordNotificationDaemon {
|
|
|
422
438
|
* legitimate create into a spurious teardown rejection.
|
|
423
439
|
*/
|
|
424
440
|
async notify(input: DiscordNotificationInput): Promise<DiscordConversation> {
|
|
425
|
-
return await this.#track(this.#notify(input));
|
|
441
|
+
return await this.#track(this.#notify(input), input.sessionId);
|
|
426
442
|
}
|
|
427
443
|
async #notify(input: DiscordNotificationInput): Promise<DiscordConversation> {
|
|
428
444
|
await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
|
|
@@ -457,7 +473,7 @@ export class DiscordNotificationDaemon {
|
|
|
457
473
|
|
|
458
474
|
/** Posts a safe command outcome to the active mapped conversation. */
|
|
459
475
|
async postCommandResult(sessionId: string, content: string): Promise<boolean> {
|
|
460
|
-
return await this.#track(this.#postCommandResult(sessionId, content));
|
|
476
|
+
return await this.#track(this.#postCommandResult(sessionId, content), sessionId);
|
|
461
477
|
}
|
|
462
478
|
async #postCommandResult(sessionId: string, content: string): Promise<boolean> {
|
|
463
479
|
const record = await this.#bySession(sessionId);
|
|
@@ -471,13 +487,20 @@ export class DiscordNotificationDaemon {
|
|
|
471
487
|
if (closing) await this.#driveClose(closing);
|
|
472
488
|
}
|
|
473
489
|
|
|
474
|
-
async recoverCleanup(sessionId: string, endpointGeneration: number): Promise<
|
|
490
|
+
async recoverCleanup(sessionId: string, endpointGeneration: number, authorityId?: string): Promise<boolean> {
|
|
475
491
|
const record = await this.#bySession(sessionId);
|
|
476
|
-
if (record?.endpointGeneration !== endpointGeneration
|
|
492
|
+
if (record?.endpointGeneration !== endpointGeneration) return true;
|
|
493
|
+
if (record?.state === "closed" || record?.state === "archived" || record?.state === "resuming") return true;
|
|
494
|
+
if (!closingIntent(record) && record?.state === "active" && record.attachmentAuthorityId === authorityId)
|
|
495
|
+
return true;
|
|
496
|
+
if (!closingIntent(record)) return false;
|
|
477
497
|
await this.#driveClose(record);
|
|
498
|
+
return true;
|
|
478
499
|
}
|
|
479
500
|
|
|
480
501
|
async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
|
|
502
|
+
await this.#invalidateSessionWork(sessionId);
|
|
503
|
+
await this.#drainSessionWork(sessionId, this.#now() + 5_000);
|
|
481
504
|
const intentKey = this.#intentKey(sessionId);
|
|
482
505
|
const now = this.#now();
|
|
483
506
|
await this.#store.transact(intentKey, current => {
|
|
@@ -548,7 +571,7 @@ export class DiscordNotificationDaemon {
|
|
|
548
571
|
endpointGeneration: number,
|
|
549
572
|
attachmentAuthorityId?: string,
|
|
550
573
|
): Promise<DiscordConversation | undefined> {
|
|
551
|
-
return await this.#track(this.#resumeAdmission(sessionId, endpointGeneration, attachmentAuthorityId));
|
|
574
|
+
return await this.#track(this.#resumeAdmission(sessionId, endpointGeneration, attachmentAuthorityId), sessionId);
|
|
552
575
|
}
|
|
553
576
|
async #resumeAdmission(
|
|
554
577
|
sessionId: string,
|
|
@@ -665,8 +688,8 @@ export class DiscordNotificationDaemon {
|
|
|
665
688
|
}
|
|
666
689
|
}
|
|
667
690
|
|
|
668
|
-
#track<T>(work: Promise<T
|
|
669
|
-
this.#activeWork.
|
|
691
|
+
#track<T>(work: Promise<T>, sessionId?: string): Promise<T> {
|
|
692
|
+
this.#activeWork.set(work, sessionId);
|
|
670
693
|
return work.finally(() => this.#activeWork.delete(work));
|
|
671
694
|
}
|
|
672
695
|
|
|
@@ -1871,6 +1894,11 @@ export class DiscordNotificationDaemon {
|
|
|
1871
1894
|
#intentKey(sessionId: string): string {
|
|
1872
1895
|
return `${this.options.provider.applicationId}:${this.options.guildId}:${this.options.parentChannelId}:creating:${sessionId}`;
|
|
1873
1896
|
}
|
|
1897
|
+
#removeSessionInvalidator(sessionId: string, set: Set<() => Promise<void>>, invalidate: () => Promise<void>): void {
|
|
1898
|
+
set.delete(invalidate);
|
|
1899
|
+
if (set.size === 0 && this.#sessionInvalidators.get(sessionId) === set)
|
|
1900
|
+
this.#sessionInvalidators.delete(sessionId);
|
|
1901
|
+
}
|
|
1874
1902
|
async #withCreateIntentLease<T>(intent: DiscordConversation, work: () => Promise<T>): Promise<T> {
|
|
1875
1903
|
let lost = false;
|
|
1876
1904
|
let renewal: Promise<boolean> | undefined;
|
|
@@ -1886,6 +1914,9 @@ export class DiscordNotificationDaemon {
|
|
|
1886
1914
|
await abandonCurrent();
|
|
1887
1915
|
};
|
|
1888
1916
|
this.#workInvalidators.add(invalidate);
|
|
1917
|
+
const sessionInvalidators = this.#sessionInvalidators.get(intent.sessionId!) ?? new Set<() => Promise<void>>();
|
|
1918
|
+
sessionInvalidators.add(invalidate);
|
|
1919
|
+
this.#sessionInvalidators.set(intent.sessionId!, sessionInvalidators);
|
|
1889
1920
|
const renew = async (): Promise<boolean> => {
|
|
1890
1921
|
if (lost || workGeneration !== this.#workGeneration) {
|
|
1891
1922
|
await invalidate();
|
|
@@ -1953,6 +1984,7 @@ export class DiscordNotificationDaemon {
|
|
|
1953
1984
|
} finally {
|
|
1954
1985
|
clearInterval(timer);
|
|
1955
1986
|
this.#workInvalidators.delete(invalidate);
|
|
1987
|
+
this.#removeSessionInvalidator(intent.sessionId!, sessionInvalidators, invalidate);
|
|
1956
1988
|
}
|
|
1957
1989
|
}
|
|
1958
1990
|
async #clearPending(record: DiscordConversation, actionId: string, actionNonce: string): Promise<void> {
|
|
@@ -2076,6 +2108,12 @@ export class DiscordNotificationDaemon {
|
|
|
2076
2108
|
);
|
|
2077
2109
|
};
|
|
2078
2110
|
this.#workInvalidators.add(invalidate);
|
|
2111
|
+
let sessionInvalidators: Set<() => Promise<void>> | undefined;
|
|
2112
|
+
if (sessionId !== undefined) {
|
|
2113
|
+
sessionInvalidators = this.#sessionInvalidators.get(sessionId) ?? new Set<() => Promise<void>>();
|
|
2114
|
+
sessionInvalidators.add(invalidate);
|
|
2115
|
+
this.#sessionInvalidators.set(sessionId, sessionInvalidators);
|
|
2116
|
+
}
|
|
2079
2117
|
const renewLease = async (): Promise<boolean> => {
|
|
2080
2118
|
if (renewalLost || workGeneration !== this.#workGeneration) {
|
|
2081
2119
|
renewalLost = true;
|
|
@@ -2136,6 +2174,8 @@ export class DiscordNotificationDaemon {
|
|
|
2136
2174
|
} finally {
|
|
2137
2175
|
clearInterval(timer);
|
|
2138
2176
|
this.#workInvalidators.delete(invalidate);
|
|
2177
|
+
if (sessionId !== undefined && sessionInvalidators !== undefined)
|
|
2178
|
+
this.#removeSessionInvalidator(sessionId, sessionInvalidators, invalidate);
|
|
2139
2179
|
}
|
|
2140
2180
|
}
|
|
2141
2181
|
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -374,6 +374,8 @@ export type LifecycleErrorReason =
|
|
|
374
374
|
| "spawn_failed"
|
|
375
375
|
| "discovery_timeout"
|
|
376
376
|
| "readiness_timeout"
|
|
377
|
+
| "worktree_preparation_timeout"
|
|
378
|
+
| "dependency_preparation_timeout"
|
|
377
379
|
| "close_refused"
|
|
378
380
|
| "not_found"
|
|
379
381
|
| "terminal_uncertain"
|
|
@@ -1223,6 +1225,8 @@ interface SessionRuntime {
|
|
|
1223
1225
|
disposeGateTerminalController: () => void;
|
|
1224
1226
|
disposeAckRecoveryParticipant: () => void;
|
|
1225
1227
|
disposeGateEmitterListener: () => void;
|
|
1228
|
+
/** Deregisters this session's job-fold listener (`bash_folded`). */
|
|
1229
|
+
disposeFoldListener: () => void;
|
|
1226
1230
|
/** Aborts and fences side turns while notification delivery is disabled. */
|
|
1227
1231
|
disableEphemeralTurns: () => void;
|
|
1228
1232
|
waitForGateResolutionQuiescence: () => Promise<void>;
|
|
@@ -2384,6 +2388,7 @@ function sdkQuerySurface(
|
|
|
2384
2388
|
steerStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown = () => ({
|
|
2385
2389
|
status: "unknown",
|
|
2386
2390
|
}),
|
|
2391
|
+
getRuntimeHost: () => SessionSdkHost | undefined = () => undefined,
|
|
2387
2392
|
): SessionSurface {
|
|
2388
2393
|
return createSdkSurfaceFactory({
|
|
2389
2394
|
ctx,
|
|
@@ -2396,6 +2401,7 @@ function sdkQuerySurface(
|
|
|
2396
2401
|
turnResultLookup,
|
|
2397
2402
|
steerStatusLookup,
|
|
2398
2403
|
hostTools: () => getInstalledDefinitions("host_tools") !== undefined,
|
|
2404
|
+
getRuntimeHost,
|
|
2399
2405
|
}).query;
|
|
2400
2406
|
}
|
|
2401
2407
|
|
|
@@ -3522,7 +3528,7 @@ function sdkControlSurface(
|
|
|
3522
3528
|
setQueueMode: (kind, mode) => {
|
|
3523
3529
|
if (!bindings.has("setQueueMode"))
|
|
3524
3530
|
return unavailable(`queue.${kind}_mode.set`, "no session queue-mode seam is installed")();
|
|
3525
|
-
if (!ctx.setQueueMode(kind as "steering" | "follow_up" | "
|
|
3531
|
+
if (!ctx.setQueueMode(kind as "steering" | "follow_up" | "tool_interrupt", mode))
|
|
3526
3532
|
throw Object.assign(new Error("Invalid queue mode."), { code: "invalid_input" });
|
|
3527
3533
|
return { changed: true };
|
|
3528
3534
|
},
|
|
@@ -4378,8 +4384,14 @@ export function createNotificationsExtension(
|
|
|
4378
4384
|
rt.workflowGate?.setRuntimeTurnProvider?.(null);
|
|
4379
4385
|
} catch {}
|
|
4380
4386
|
try {
|
|
4387
|
+
const attachedClients = rt.server.clientCount();
|
|
4381
4388
|
const delivered = await pushTerminalSessionFrame(rt, { type: "session_closed", sessionId: id });
|
|
4382
|
-
if (!delivered)
|
|
4389
|
+
if (!delivered) {
|
|
4390
|
+
const fields = { sessionId: id, endpointScope: rt.endpointScope, attachedClients };
|
|
4391
|
+
if (attachedClients > 0)
|
|
4392
|
+
logger.warn("notifications: session_closed socket delivery was not acknowledged", fields);
|
|
4393
|
+
else logger.debug("notifications: session_closed had no attached client to acknowledge", fields);
|
|
4394
|
+
}
|
|
4383
4395
|
} catch (e) {
|
|
4384
4396
|
logger.warn(`notifications: session_closed failed: ${String(e)}`);
|
|
4385
4397
|
}
|
|
@@ -4390,6 +4402,9 @@ export function createNotificationsExtension(
|
|
|
4390
4402
|
try {
|
|
4391
4403
|
rt.disposeGateEmitterListener();
|
|
4392
4404
|
} catch {}
|
|
4405
|
+
try {
|
|
4406
|
+
rt.disposeFoldListener();
|
|
4407
|
+
} catch {}
|
|
4393
4408
|
rt.gatePresentations?.dispose();
|
|
4394
4409
|
try {
|
|
4395
4410
|
rt.disposeGateTerminalController();
|
|
@@ -5282,7 +5297,6 @@ export function createNotificationsExtension(
|
|
|
5282
5297
|
submission.reconciliationKind,
|
|
5283
5298
|
correlation,
|
|
5284
5299
|
winner,
|
|
5285
|
-
undefined,
|
|
5286
5300
|
extra?.error,
|
|
5287
5301
|
extra?.finalText,
|
|
5288
5302
|
);
|
|
@@ -5454,6 +5468,7 @@ export function createNotificationsExtension(
|
|
|
5454
5468
|
settings,
|
|
5455
5469
|
selector => kindReconciliation.lookupResult(selector.kind, selector),
|
|
5456
5470
|
selector => kindReconciliation.lookupSteer(selector),
|
|
5471
|
+
() => host,
|
|
5457
5472
|
),
|
|
5458
5473
|
id,
|
|
5459
5474
|
revisions,
|
|
@@ -6985,6 +7000,7 @@ export function createNotificationsExtension(
|
|
|
6985
7000
|
]);
|
|
6986
7001
|
},
|
|
6987
7002
|
disposeGateEmitterListener: () => {},
|
|
7003
|
+
disposeFoldListener: () => {},
|
|
6988
7004
|
trackGateResolution,
|
|
6989
7005
|
waitForGateResolutionQuiescence: async () => {
|
|
6990
7006
|
await Promise.allSettled(inFlightGateResolutions);
|
|
@@ -8009,6 +8025,20 @@ export function createNotificationsExtension(
|
|
|
8009
8025
|
};
|
|
8010
8026
|
activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
|
|
8011
8027
|
if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
|
|
8028
|
+
// Every fold (steer, chord, auto-background timer, `bash.background`)
|
|
8029
|
+
// publishes one replayable `bash_folded` frame. `runtime.jobs.list`
|
|
8030
|
+
// `foldReason` remains the source of truth clients reconcile against after
|
|
8031
|
+
// a replay, since a replayed frame may describe a job that has since ended.
|
|
8032
|
+
activeRuntime.disposeFoldListener =
|
|
8033
|
+
ctx.onJobFold?.(event => {
|
|
8034
|
+
emitSessionEvent(activeRuntime, {
|
|
8035
|
+
type: "bash_folded",
|
|
8036
|
+
sessionId: id,
|
|
8037
|
+
jobId: event.jobId,
|
|
8038
|
+
generation: event.generation,
|
|
8039
|
+
reason: event.reason,
|
|
8040
|
+
});
|
|
8041
|
+
}) ?? (() => {});
|
|
8012
8042
|
finishStartup({ status: "started", runtime: initializedRuntime });
|
|
8013
8043
|
return { status: "started", runtime: initializedRuntime };
|
|
8014
8044
|
} catch (e) {
|