@gajae-code/coding-agent 0.15.5 → 0.15.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 +26 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +2 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +96 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +15 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -7
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +40 -5
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/config/autorouting-tier-map.ts +32 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +51 -9
- package/src/config/model-registry.ts +1021 -122
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/cli/session-cli.ts +12 -1
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +371 -37
- package/src/sdk/host/session-runtime.ts +364 -52
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +228 -96
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +678 -103
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +5 -4
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +772 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
|
|
4
|
-
import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
|
|
4
|
+
import { type AutocompleteProvider, getKeybindings, matchesKey, type SlashCommand } from "@gajae-code/tui";
|
|
5
5
|
import { $pickenv, logger, sanitizeText } from "@gajae-code/utils";
|
|
6
6
|
import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
|
|
7
7
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
@@ -1855,19 +1855,36 @@ export class InputController {
|
|
|
1855
1855
|
const now = Date.now();
|
|
1856
1856
|
if (now - this.#lastBackgroundFoldKeyTime > BACKGROUND_FOLD_DOUBLE_PRESS_MS) {
|
|
1857
1857
|
this.#lastBackgroundFoldKeyTime = now;
|
|
1858
|
-
|
|
1858
|
+
const foldKeys = getKeybindings().getKeys("app.tool.backgroundFold");
|
|
1859
|
+
this.ctx.showStatus(
|
|
1860
|
+
`Press ${foldKeys.join(" / ")} again to fold supported foreground bash into a background job`,
|
|
1861
|
+
);
|
|
1859
1862
|
return true;
|
|
1860
1863
|
}
|
|
1861
1864
|
this.#lastBackgroundFoldKeyTime = 0;
|
|
1862
1865
|
|
|
1863
|
-
|
|
1866
|
+
try {
|
|
1867
|
+
const request = this.ctx.session.requestForegroundBashBackground?.() ?? Promise.resolve(false);
|
|
1868
|
+
void request
|
|
1869
|
+
.then(backgrounded => {
|
|
1870
|
+
if (backgrounded) {
|
|
1871
|
+
this.ctx.showStatus("Folding foreground bash into a quiet background job…");
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
this.ctx.showWarning(
|
|
1875
|
+
"No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
|
|
1876
|
+
);
|
|
1877
|
+
})
|
|
1878
|
+
.catch(() => {
|
|
1879
|
+
this.ctx.showWarning(
|
|
1880
|
+
"No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
|
|
1881
|
+
);
|
|
1882
|
+
});
|
|
1883
|
+
} catch {
|
|
1864
1884
|
this.ctx.showWarning(
|
|
1865
1885
|
"No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
|
|
1866
1886
|
);
|
|
1867
|
-
return true;
|
|
1868
1887
|
}
|
|
1869
|
-
|
|
1870
|
-
this.ctx.showStatus("Folding foreground bash into a quiet background job…");
|
|
1871
1888
|
return true;
|
|
1872
1889
|
}
|
|
1873
1890
|
|
|
@@ -426,6 +426,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
426
426
|
lastComposerClearEscapeTime = 0;
|
|
427
427
|
shutdownRequested = false;
|
|
428
428
|
#isShuttingDown = false;
|
|
429
|
+
#shutdownEscalated = false;
|
|
429
430
|
hookSelector: HookSelectorComponent | undefined = undefined;
|
|
430
431
|
hookInput: HookInputComponent | undefined = undefined;
|
|
431
432
|
hookEditor: HookEditorComponent | undefined = undefined;
|
|
@@ -1682,7 +1683,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1682
1683
|
}
|
|
1683
1684
|
|
|
1684
1685
|
async shutdown(): Promise<void> {
|
|
1685
|
-
if (this.#isShuttingDown)
|
|
1686
|
+
if (this.#isShuttingDown) {
|
|
1687
|
+
// A second explicit exit is an escalation, not another silent no-op. The
|
|
1688
|
+
// graceful path may be waiting on a lock-hostile filesystem; postmortem.quit
|
|
1689
|
+
// has its own bounded cleanup deadline and will force the process out even
|
|
1690
|
+
// when the first shutdown cannot finish.
|
|
1691
|
+
if (!this.#shutdownEscalated) {
|
|
1692
|
+
this.#shutdownEscalated = true;
|
|
1693
|
+
logger.warn("Escalating repeated interactive shutdown request");
|
|
1694
|
+
this.session.abort();
|
|
1695
|
+
void postmortem.quit(1);
|
|
1696
|
+
}
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1686
1699
|
this.#isShuttingDown = true;
|
|
1687
1700
|
|
|
1688
1701
|
// `/btw` owns the shared composer while its panel is open. Never persist a
|
|
@@ -16,12 +16,48 @@ import type { AsyncJob, AsyncJobManager } from "../async";
|
|
|
16
16
|
import { deleteCronJobById, listCronSnapshots, onCronChange } from "../tools/cron";
|
|
17
17
|
|
|
18
18
|
export type JobsWorstState = "none" | "running" | "failed";
|
|
19
|
+
export type JobDeliveryState = "pending" | "delivered" | "failed-visible";
|
|
20
|
+
|
|
21
|
+
/** The manager-owned, one-pass job projection consumed by this observer. */
|
|
22
|
+
export interface AsyncJobSnapshotEntry {
|
|
23
|
+
id: string;
|
|
24
|
+
kind: string;
|
|
25
|
+
label: string;
|
|
26
|
+
status: AsyncJob["status"];
|
|
27
|
+
generation: string;
|
|
28
|
+
backgrounded: boolean;
|
|
29
|
+
deliveryState: JobDeliveryState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Scalar delivery failure evidence that may outlive the AsyncJob record. */
|
|
33
|
+
export interface DeadLetteredJobSnapshotEntry {
|
|
34
|
+
jobId: string;
|
|
35
|
+
generation: string;
|
|
36
|
+
ownerId?: string;
|
|
37
|
+
backgrounded?: boolean;
|
|
38
|
+
attempt: number;
|
|
39
|
+
lastError?: string;
|
|
40
|
+
recordedAt: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AsyncJobsSnapshot {
|
|
44
|
+
jobs: AsyncJobSnapshotEntry[];
|
|
45
|
+
deadLettered: DeadLetteredJobSnapshotEntry[];
|
|
46
|
+
}
|
|
19
47
|
|
|
20
48
|
export interface MonitorJobView {
|
|
21
49
|
id: string;
|
|
22
50
|
label: string;
|
|
23
51
|
status: AsyncJob["status"];
|
|
24
52
|
startTime: number;
|
|
53
|
+
generation?: string;
|
|
54
|
+
backgrounded?: boolean;
|
|
55
|
+
deliveryState?: JobDeliveryState;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FoldedJobView extends AsyncJobSnapshotEntry {
|
|
59
|
+
/** Safe diagnostic retained for a dead-lettered delivery, when available. */
|
|
60
|
+
errorText?: string;
|
|
25
61
|
}
|
|
26
62
|
|
|
27
63
|
export interface CronJobView {
|
|
@@ -39,6 +75,10 @@ export interface CronJobView {
|
|
|
39
75
|
export interface JobsSnapshot {
|
|
40
76
|
monitors: MonitorJobView[];
|
|
41
77
|
crons: CronJobView[];
|
|
78
|
+
/** Backgrounded work and undeliverable terminal work that must stay visible. */
|
|
79
|
+
foldedJobs?: FoldedJobView[];
|
|
80
|
+
/** Scalar dead-letter evidence retained for diagnostics and red-drill-in. */
|
|
81
|
+
deadLettered?: DeadLetteredJobSnapshotEntry[];
|
|
42
82
|
activeMonitorCount: number;
|
|
43
83
|
activeCronCount: number;
|
|
44
84
|
worstState: JobsWorstState;
|
|
@@ -48,12 +88,22 @@ export interface JobsSnapshot {
|
|
|
48
88
|
export const EMPTY_JOBS_SNAPSHOT: JobsSnapshot = {
|
|
49
89
|
monitors: [],
|
|
50
90
|
crons: [],
|
|
91
|
+
foldedJobs: [],
|
|
92
|
+
deadLettered: [],
|
|
51
93
|
activeMonitorCount: 0,
|
|
52
94
|
activeCronCount: 0,
|
|
53
95
|
worstState: "none",
|
|
54
96
|
failedUnacknowledged: false,
|
|
55
97
|
};
|
|
56
98
|
|
|
99
|
+
function jobKey(id: string, generation: string): string {
|
|
100
|
+
return `${id}\u0000${generation}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface AsyncJobSnapshotSource {
|
|
104
|
+
getJobsSnapshot?: (filter?: { ownerId?: string }) => AsyncJobsSnapshot;
|
|
105
|
+
}
|
|
106
|
+
|
|
57
107
|
export class JobsObserver {
|
|
58
108
|
readonly #manager: AsyncJobManager;
|
|
59
109
|
readonly #ownerId: string | undefined;
|
|
@@ -103,7 +153,14 @@ export class JobsObserver {
|
|
|
103
153
|
}
|
|
104
154
|
}
|
|
105
155
|
|
|
156
|
+
#readAsyncJobsSnapshot(): AsyncJobsSnapshot {
|
|
157
|
+
const source = this.#manager as AsyncJobSnapshotSource;
|
|
158
|
+
if (typeof source.getJobsSnapshot !== "function") return { jobs: [], deadLettered: [] };
|
|
159
|
+
return source.getJobsSnapshot(this.#ownerId ? { ownerId: this.#ownerId } : undefined);
|
|
160
|
+
}
|
|
161
|
+
|
|
106
162
|
#listMonitorJobs(): AsyncJob[] {
|
|
163
|
+
if (typeof this.#manager.getAllJobs !== "function") return [];
|
|
107
164
|
const filter = this.#ownerId ? { ownerId: this.#ownerId } : undefined;
|
|
108
165
|
return this.#manager.getAllJobs(filter).filter(job => job.type === "bash" && job.metadata?.monitor === true);
|
|
109
166
|
}
|
|
@@ -114,25 +171,92 @@ export class JobsObserver {
|
|
|
114
171
|
* snapshot (never scans the manager/cron stores).
|
|
115
172
|
*/
|
|
116
173
|
#recompute(): void {
|
|
174
|
+
const asyncSnapshot = this.#readAsyncJobsSnapshot();
|
|
175
|
+
const snapshotEntries = new Map<string, AsyncJobSnapshotEntry>();
|
|
176
|
+
for (const entry of asyncSnapshot.jobs) snapshotEntries.set(jobKey(entry.id, entry.generation), entry);
|
|
177
|
+
|
|
117
178
|
const monitorJobs = this.#listMonitorJobs();
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
179
|
+
const monitorKeys = new Set(monitorJobs.map(job => jobKey(job.id, job.generation)));
|
|
180
|
+
const presentKeys = new Set<string>([
|
|
181
|
+
...asyncSnapshot.jobs.map(entry => jobKey(entry.id, entry.generation)),
|
|
182
|
+
...asyncSnapshot.deadLettered.map(entry => jobKey(entry.jobId, entry.generation)),
|
|
183
|
+
]);
|
|
184
|
+
// Prune acknowledged keys whose jobs and scalar dead letters have been evicted.
|
|
185
|
+
for (const key of this.#acknowledgedFailedIds) {
|
|
186
|
+
if (!presentKeys.has(key)) this.#acknowledgedFailedIds.delete(key);
|
|
122
187
|
}
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
const hasUnacknowledgedFailure =
|
|
127
|
-
|
|
128
|
-
|
|
188
|
+
|
|
189
|
+
// Sticky failure latch: the manager's terminal status and delivery
|
|
190
|
+
// classification are authoritative; neither is reconstructed from getters.
|
|
191
|
+
const hasUnacknowledgedFailure =
|
|
192
|
+
asyncSnapshot.jobs.some(
|
|
193
|
+
entry =>
|
|
194
|
+
(entry.deliveryState === "failed-visible" ||
|
|
195
|
+
(entry.status === "failed" &&
|
|
196
|
+
(entry.backgrounded || monitorKeys.has(jobKey(entry.id, entry.generation))))) &&
|
|
197
|
+
!this.#acknowledgedFailedIds.has(jobKey(entry.id, entry.generation)),
|
|
198
|
+
) ||
|
|
199
|
+
asyncSnapshot.deadLettered.some(
|
|
200
|
+
entry => !this.#acknowledgedFailedIds.has(jobKey(entry.jobId, entry.generation)),
|
|
201
|
+
);
|
|
129
202
|
if (hasUnacknowledgedFailure) this.#failedUnacknowledged = true;
|
|
130
203
|
|
|
131
|
-
const activeMonitors = monitorJobs.filter(job => job.status === "running");
|
|
132
|
-
const cronSnapshots = listCronSnapshots(this.#ownerId);
|
|
133
204
|
const monitors: MonitorJobView[] = monitorJobs
|
|
134
|
-
.
|
|
205
|
+
.flatMap(job => {
|
|
206
|
+
const entry = snapshotEntries.get(jobKey(job.id, job.generation));
|
|
207
|
+
if (!entry) return [];
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
id: entry.id,
|
|
211
|
+
label: entry.label,
|
|
212
|
+
status: entry.status,
|
|
213
|
+
startTime: job.startTime,
|
|
214
|
+
generation: entry.generation,
|
|
215
|
+
backgrounded: entry.backgrounded,
|
|
216
|
+
deliveryState: entry.deliveryState,
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
})
|
|
135
220
|
.sort((a, b) => b.startTime - a.startTime);
|
|
221
|
+
|
|
222
|
+
// Keep every backgrounded job visible. A non-delivered terminal entry is
|
|
223
|
+
// also retained here so pending/failed delivery can never disappear merely
|
|
224
|
+
// because it was not marked backgrounded by its producer.
|
|
225
|
+
const deadLettersByKey = new Map(
|
|
226
|
+
asyncSnapshot.deadLettered.map(entry => [jobKey(entry.jobId, entry.generation), entry] as const),
|
|
227
|
+
);
|
|
228
|
+
const foldedJobs: FoldedJobView[] = asyncSnapshot.jobs
|
|
229
|
+
.filter(
|
|
230
|
+
entry =>
|
|
231
|
+
entry.backgrounded ||
|
|
232
|
+
(entry.status !== "running" &&
|
|
233
|
+
(entry.deliveryState === "failed-visible" ||
|
|
234
|
+
(entry.status === "failed" && entry.backgrounded) ||
|
|
235
|
+
entry.deliveryState !== "delivered")),
|
|
236
|
+
)
|
|
237
|
+
.map(entry => {
|
|
238
|
+
const lastError = deadLettersByKey.get(jobKey(entry.id, entry.generation))?.lastError;
|
|
239
|
+
return lastError === undefined ? { ...entry } : { ...entry, errorText: lastError };
|
|
240
|
+
});
|
|
241
|
+
const foldedKeys = new Set(foldedJobs.map(entry => jobKey(entry.id, entry.generation)));
|
|
242
|
+
for (const entry of asyncSnapshot.deadLettered) {
|
|
243
|
+
const key = jobKey(entry.jobId, entry.generation);
|
|
244
|
+
if (foldedKeys.has(key)) continue;
|
|
245
|
+
foldedKeys.add(key);
|
|
246
|
+
foldedJobs.push({
|
|
247
|
+
id: entry.jobId,
|
|
248
|
+
kind: "dead-letter",
|
|
249
|
+
label: entry.jobId,
|
|
250
|
+
status: "failed",
|
|
251
|
+
generation: entry.generation,
|
|
252
|
+
backgrounded: entry.backgrounded === true,
|
|
253
|
+
deliveryState: "failed-visible",
|
|
254
|
+
errorText: entry.lastError,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const activeMonitors = monitors.filter(monitor => monitor.status === "running" && !monitor.backgrounded);
|
|
259
|
+
const cronSnapshots = listCronSnapshots(this.#ownerId);
|
|
136
260
|
const crons: CronJobView[] = cronSnapshots
|
|
137
261
|
.map(snapshot => ({
|
|
138
262
|
id: snapshot.id,
|
|
@@ -145,14 +269,17 @@ export class JobsObserver {
|
|
|
145
269
|
firing: snapshot.firing,
|
|
146
270
|
}))
|
|
147
271
|
.sort((a, b) => b.createdAt - a.createdAt);
|
|
272
|
+
const hasRunningFoldedJob = foldedJobs.some(job => job.status === "running");
|
|
148
273
|
const worstState: JobsWorstState = this.#failedUnacknowledged
|
|
149
274
|
? "failed"
|
|
150
|
-
: activeMonitors.length > 0 || crons.length > 0
|
|
275
|
+
: activeMonitors.length > 0 || crons.length > 0 || hasRunningFoldedJob
|
|
151
276
|
? "running"
|
|
152
277
|
: "none";
|
|
153
278
|
this.#snapshot = {
|
|
154
279
|
monitors,
|
|
155
280
|
crons,
|
|
281
|
+
foldedJobs,
|
|
282
|
+
deadLettered: asyncSnapshot.deadLettered.map(entry => ({ ...entry })),
|
|
156
283
|
activeMonitorCount: activeMonitors.length,
|
|
157
284
|
activeCronCount: crons.length,
|
|
158
285
|
worstState,
|
|
@@ -167,8 +294,14 @@ export class JobsObserver {
|
|
|
167
294
|
|
|
168
295
|
/** Clear the failure latch (called when the user opens the jobs overlay). */
|
|
169
296
|
acknowledgeFailures(): void {
|
|
170
|
-
|
|
171
|
-
|
|
297
|
+
const asyncSnapshot = this.#readAsyncJobsSnapshot();
|
|
298
|
+
for (const entry of asyncSnapshot.jobs) {
|
|
299
|
+
if (entry.status === "failed" || entry.deliveryState === "failed-visible") {
|
|
300
|
+
this.#acknowledgedFailedIds.add(jobKey(entry.id, entry.generation));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
for (const entry of asyncSnapshot.deadLettered) {
|
|
304
|
+
this.#acknowledgedFailedIds.add(jobKey(entry.jobId, entry.generation));
|
|
172
305
|
}
|
|
173
306
|
if (!this.#failedUnacknowledged) return;
|
|
174
307
|
this.#failedUnacknowledged = false;
|
|
@@ -343,7 +343,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
|
|
343
343
|
return { retried: true, immediate: true };
|
|
344
344
|
}
|
|
345
345
|
case "bash.background": {
|
|
346
|
-
if (!session.requestForegroundBashBackground())
|
|
346
|
+
if (!(await session.requestForegroundBashBackground()))
|
|
347
347
|
throw Object.assign(
|
|
348
348
|
new Error("The active bash command cannot be moved to a managed background job."),
|
|
349
349
|
{ code: "not_foldable" },
|
|
@@ -17,7 +17,11 @@ import type { InteractiveModeContext } from "../types";
|
|
|
17
17
|
* (unhandled rejection → resident crash).
|
|
18
18
|
*/
|
|
19
19
|
export function extensionUserMessageContentError(content: string | (TextContent | ImageContent)[]): Error | undefined {
|
|
20
|
-
if (typeof content === "string")
|
|
20
|
+
if (typeof content === "string") {
|
|
21
|
+
if (content.trim().length === 0)
|
|
22
|
+
return Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
21
25
|
if (!Array.isArray(content)) {
|
|
22
26
|
return Object.assign(new Error("sendUserMessage requires string or content-array content."), {
|
|
23
27
|
code: "invalid_input",
|
|
@@ -30,6 +34,16 @@ export function extensionUserMessageContentError(content: string | (TextContent
|
|
|
30
34
|
});
|
|
31
35
|
}
|
|
32
36
|
}
|
|
37
|
+
const text = content
|
|
38
|
+
.filter((part): part is TextContent => part.type === "text")
|
|
39
|
+
.map(part => part.text)
|
|
40
|
+
.join("\n");
|
|
41
|
+
const hasUsableImage = content.some(
|
|
42
|
+
(part): part is ImageContent =>
|
|
43
|
+
part.type === "image" && typeof part.data === "string" && part.data.trim().length > 0,
|
|
44
|
+
);
|
|
45
|
+
if (text.trim().length === 0 && !hasUsableImage)
|
|
46
|
+
return Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
33
47
|
return undefined;
|
|
34
48
|
}
|
|
35
49
|
export function normalizeInjectedUserContent(content: string | (TextContent | ImageContent)[]): {
|
|
@@ -16,8 +16,8 @@ Executes bash command in shell session for terminal operations like git, bun, ca
|
|
|
16
16
|
{{#if asyncEnabled}}
|
|
17
17
|
- Use `async: true` for long-running commands when you don't need immediate output; the call returns a background job ID and the result is delivered automatically as a follow-up.
|
|
18
18
|
{{/if}}
|
|
19
|
-
{{#if
|
|
20
|
-
- In the interactive TUI, the user can press `
|
|
19
|
+
{{#if foregroundFoldEnabled}}
|
|
20
|
+
- In the interactive TUI, the user can press the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via the `app.tool.backgroundFold` keybinding) while a foreground command is still running to fold it into a background job. All three command surfaces fold: managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded command keeps its original timeout and is never killed or restarted by folding; a folded PTY continues output-only, so it stops accepting input. The turn then ends cleanly and a later turn resumes automatically with the command's result, so the interrupted task is finished rather than abandoned. `task`/`subagent` waits are deliberately NOT foldable. Do not instruct users to use raw shell `Ctrl+Z`/`bg` inside the GJC TUI; ownership and output routing are not safe there.
|
|
21
21
|
{{/if}}
|
|
22
22
|
</instruction>
|
|
23
23
|
{{#if restrictedAllowedPrefixes}}
|
package/src/prompts/tools/job.md
CHANGED
|
@@ -2,7 +2,7 @@ Inspects, waits, or cancels async jobs.
|
|
|
2
2
|
|
|
3
3
|
Background job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.
|
|
4
4
|
|
|
5
|
-
In the interactive TUI,
|
|
5
|
+
In the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.
|
|
6
6
|
|
|
7
7
|
# Operations
|
|
8
8
|
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
type SessionReconcileUncertainTarget,
|
|
10
10
|
validateSessionReconcileUncertainTarget,
|
|
11
11
|
} from "../lifecycle/service";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
validateAdapterControl,
|
|
14
|
+
validateAdapterSecretFields,
|
|
15
|
+
validateRequiredPromptText,
|
|
16
|
+
} from "../protocol/adapter-validation";
|
|
13
17
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
14
18
|
import { type SessionAttachment, type SessionRouter, SessionRouterError } from "../router";
|
|
15
19
|
import { ACP_SESSION_RECONNECT, SESSION_ABORT_TIMEOUT_MS } from "../session-reconnect";
|
|
@@ -343,7 +347,18 @@ export class AcpSdkAdapter {
|
|
|
343
347
|
}
|
|
344
348
|
|
|
345
349
|
async prompt(params: JsonObject | string): Promise<unknown> {
|
|
346
|
-
const
|
|
350
|
+
const rawText =
|
|
351
|
+
typeof params === "string"
|
|
352
|
+
? params
|
|
353
|
+
: params !== null && typeof params === "object"
|
|
354
|
+
? (params.prompt ?? params.text)
|
|
355
|
+
: undefined;
|
|
356
|
+
const text = typeof rawText === "string" ? rawText : "";
|
|
357
|
+
const invalid = validateRequiredPromptText("turn.prompt", {
|
|
358
|
+
text,
|
|
359
|
+
...(typeof params === "object" && Array.isArray(params.images) ? { images: params.images } : {}),
|
|
360
|
+
});
|
|
361
|
+
if (invalid) throw new AcpSdkAdapterError(invalid.code, invalid.message);
|
|
347
362
|
return await this.#requestSession({
|
|
348
363
|
type: "control_request",
|
|
349
364
|
operation: "turn.prompt",
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface BrokerSettings {
|
|
|
46
46
|
heartbeatTtlMs?: number;
|
|
47
47
|
/** Broker-owned migration policy. Client lifecycle frames cannot select it. */
|
|
48
48
|
resolveDirectoryMigration?: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
|
|
49
|
+
/** Host model resolver override for lifecycle tests and embedders. */
|
|
50
|
+
resolveModelPin?: SdkHostModelResolver;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
type ResolvedBrokerSettings = {
|
|
@@ -863,7 +865,8 @@ export class Broker {
|
|
|
863
865
|
#completion!: Promise<void>;
|
|
864
866
|
#resolveCompletion!: () => void;
|
|
865
867
|
#rejectCompletion!: (error: unknown) => void;
|
|
866
|
-
|
|
868
|
+
#resolveModelPin: SdkHostModelResolver;
|
|
869
|
+
#ownsResolveModelPin: boolean;
|
|
867
870
|
constructor(settings: BrokerSettings) {
|
|
868
871
|
this.settings = {
|
|
869
872
|
agentDir: settings.agentDir,
|
|
@@ -874,7 +877,8 @@ export class Broker {
|
|
|
874
877
|
};
|
|
875
878
|
this.index = new SessionIndex(settings.agentDir);
|
|
876
879
|
this.ledger = new LifecycleLedger(settings.agentDir);
|
|
877
|
-
this.#
|
|
880
|
+
this.#ownsResolveModelPin = settings.resolveModelPin === undefined;
|
|
881
|
+
this.#resolveModelPin = settings.resolveModelPin ?? createDefaultSdkHostModelResolver(this.settings.agentDir);
|
|
878
882
|
this.#lock = path.join(settings.agentDir, "sdk", "broker.lock");
|
|
879
883
|
const completion = Promise.withResolvers<void>();
|
|
880
884
|
this.#completion = completion.promise;
|
|
@@ -1023,6 +1027,8 @@ export class Broker {
|
|
|
1023
1027
|
this.#resolveCompletion = completion.resolve;
|
|
1024
1028
|
this.#rejectCompletion = completion.reject;
|
|
1025
1029
|
this.#completionTask = null;
|
|
1030
|
+
if (this.#ownsResolveModelPin)
|
|
1031
|
+
this.#resolveModelPin = createDefaultSdkHostModelResolver(this.settings.agentDir);
|
|
1026
1032
|
// A drained queue refuses every later startup by design, so a restarted broker
|
|
1027
1033
|
// needs a new one or it would admit nothing for the rest of the process.
|
|
1028
1034
|
this.#startupAdmissions = new StartupAdmissionQueue(sdkHostStartupConcurrency());
|
|
@@ -1285,25 +1291,35 @@ export class Broker {
|
|
|
1285
1291
|
if (this.#heartbeatTimer) clearInterval(this.#heartbeatTimer);
|
|
1286
1292
|
this.#heartbeatTimer = null;
|
|
1287
1293
|
this.#completionTask = (async () => {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1294
|
+
try {
|
|
1295
|
+
await this.#transport?.stop();
|
|
1296
|
+
this.#transport = null;
|
|
1297
|
+
if (mode === "lost-root")
|
|
1298
|
+
await Promise.race([Promise.allSettled(this.#admitted), Bun.sleep(BROKER_SETTLEMENT_MS)]);
|
|
1299
|
+
else await Promise.allSettled(this.#admitted);
|
|
1300
|
+
this.#publication?.close();
|
|
1301
|
+
this.#publication = null;
|
|
1302
|
+
if (mode === "owned-root" && this.discovery?.ownerId === this.#owner) {
|
|
1303
|
+
try {
|
|
1304
|
+
const disk = JSON.parse(await fs.readFile(brokerDiscoveryPath(this.settings.agentDir), "utf8")) as {
|
|
1305
|
+
ownerId?: string;
|
|
1306
|
+
};
|
|
1307
|
+
if (disk.ownerId === this.#owner) await fs.unlink(brokerDiscoveryPath(this.settings.agentDir));
|
|
1308
|
+
} catch (e) {
|
|
1309
|
+
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
|
|
1310
|
+
}
|
|
1311
|
+
await this.#releaseOwnedLock();
|
|
1312
|
+
}
|
|
1313
|
+
} finally {
|
|
1314
|
+
const disposeModelPin = this.#ownsResolveModelPin ? this.#resolveModelPin.dispose?.() : undefined;
|
|
1315
|
+
if (mode === "lost-root" && disposeModelPin !== undefined) {
|
|
1316
|
+
void disposeModelPin.catch(() => undefined);
|
|
1317
|
+
await Promise.race([disposeModelPin, Bun.sleep(BROKER_SETTLEMENT_MS)]);
|
|
1318
|
+
} else {
|
|
1319
|
+
await disposeModelPin;
|
|
1303
1320
|
}
|
|
1304
|
-
|
|
1321
|
+
this.discovery = null;
|
|
1305
1322
|
}
|
|
1306
|
-
this.discovery = null;
|
|
1307
1323
|
})();
|
|
1308
1324
|
void this.#completionTask.then(this.#resolveCompletion, this.#rejectCompletion);
|
|
1309
1325
|
return this.#completionTask;
|
|
@@ -1417,14 +1433,25 @@ export class Broker {
|
|
|
1417
1433
|
|
|
1418
1434
|
try {
|
|
1419
1435
|
const endpointPath = path.join(record.locator.stateRoot, "sdk", `${record.sessionId}.json`);
|
|
1420
|
-
const
|
|
1436
|
+
const before = await fs.lstat(endpointPath, { bigint: true });
|
|
1437
|
+
if (!before.isFile()) return error("endpoint_stale", "session endpoint is not a regular file");
|
|
1438
|
+
const source = await fs.readFile(endpointPath, "utf8");
|
|
1439
|
+
const metadata = await fs.lstat(endpointPath, { bigint: true });
|
|
1440
|
+
if (
|
|
1441
|
+
before.dev !== metadata.dev ||
|
|
1442
|
+
before.ino !== metadata.ino ||
|
|
1443
|
+
before.size !== metadata.size ||
|
|
1444
|
+
before.mtimeNs !== metadata.mtimeNs ||
|
|
1445
|
+
before.ctimeNs !== metadata.ctimeNs
|
|
1446
|
+
)
|
|
1447
|
+
return error("endpoint_stale", "session endpoint changed during read");
|
|
1421
1448
|
const endpoint = JSON.parse(source) as Record<string, unknown>;
|
|
1422
1449
|
if (
|
|
1423
1450
|
endpoint.sessionId !== record.sessionId ||
|
|
1424
1451
|
endpoint.pid !== record.pid ||
|
|
1425
1452
|
endpoint.stale === true ||
|
|
1426
1453
|
record.endpointMtimeMs === undefined ||
|
|
1427
|
-
Math.abs(metadata.
|
|
1454
|
+
Math.abs(Number(metadata.mtimeNs) / 1_000_000 - record.endpointMtimeMs) > 0.001
|
|
1428
1455
|
)
|
|
1429
1456
|
return error("endpoint_stale", "session endpoint is stale");
|
|
1430
1457
|
await this.index.refresh();
|
|
@@ -14,7 +14,10 @@ function nativeLifecycle(): typeof import("@gajae-code/natives") {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
import { $credentialEnv, logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
17
|
-
|
|
17
|
+
import {
|
|
18
|
+
loadAcceptedModelPresetRegistry,
|
|
19
|
+
loadAcceptedModelPresetRegistryAsync,
|
|
20
|
+
} from "../../config/model-preset-registry";
|
|
18
21
|
import {
|
|
19
22
|
isModelProfileError,
|
|
20
23
|
type ModelProfileErrorDetails,
|
|
@@ -609,13 +612,36 @@ function text(value: unknown): string | undefined {
|
|
|
609
612
|
return typeof value === "string" && value ? value : undefined;
|
|
610
613
|
}
|
|
611
614
|
|
|
612
|
-
function
|
|
615
|
+
function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
616
|
+
const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
|
|
617
|
+
modelsConfigFile.invalidate();
|
|
618
|
+
const loaded = modelsConfigFile.tryLoad();
|
|
619
|
+
const accepted = loadAcceptedModelPresetRegistry(agentDir);
|
|
620
|
+
const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined, accepted.profiles);
|
|
621
|
+
try {
|
|
622
|
+
return validateModelProfileName(
|
|
623
|
+
requestedProfile,
|
|
624
|
+
profiles,
|
|
625
|
+
accepted.error ?? (loaded.status === "error" ? loaded.error : undefined),
|
|
626
|
+
);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (isModelProfileError(error)) return fail(error.code, error.message, undefined, error.details);
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function validateBrokerModelPreset(agentDir: string, requestedProfile: string): Promise<string | BrokerResponse> {
|
|
613
634
|
const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
|
|
614
635
|
modelsConfigFile.invalidate();
|
|
615
636
|
const loaded = modelsConfigFile.tryLoad();
|
|
616
|
-
const
|
|
637
|
+
const accepted = await loadAcceptedModelPresetRegistryAsync(agentDir);
|
|
638
|
+
const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined, accepted.profiles);
|
|
617
639
|
try {
|
|
618
|
-
return validateModelProfileName(
|
|
640
|
+
return validateModelProfileName(
|
|
641
|
+
requestedProfile,
|
|
642
|
+
profiles,
|
|
643
|
+
accepted.error ?? (loaded.status === "error" ? loaded.error : undefined),
|
|
644
|
+
);
|
|
619
645
|
} catch (error) {
|
|
620
646
|
if (isModelProfileError(error)) return fail(error.code, error.message, undefined, error.details);
|
|
621
647
|
throw error;
|
|
@@ -623,7 +649,7 @@ function validateBrokerModelPreset(agentDir: string, requestedProfile: string):
|
|
|
623
649
|
}
|
|
624
650
|
|
|
625
651
|
export function validateBrokerModelPresetForTest(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
626
|
-
return
|
|
652
|
+
return validateBrokerModelPresetSync(agentDir, requestedProfile);
|
|
627
653
|
}
|
|
628
654
|
|
|
629
655
|
function readinessTimeout(input: Input): number | BrokerResponse {
|
|
@@ -4030,7 +4056,7 @@ async function launchInput(
|
|
|
4030
4056
|
if (input.modelPreset !== undefined && (typeof input.modelPreset !== "string" || input.modelPreset.length === 0))
|
|
4031
4057
|
return fail("invalid_input", "modelPreset must be a non-empty exact profile ID.");
|
|
4032
4058
|
if (modelPreset !== undefined) {
|
|
4033
|
-
const validatedModelPreset = validateBrokerModelPreset(broker.settings.agentDir, modelPreset);
|
|
4059
|
+
const validatedModelPreset = await validateBrokerModelPreset(broker.settings.agentDir, modelPreset);
|
|
4034
4060
|
if (typeof validatedModelPreset !== "string") return validatedModelPreset;
|
|
4035
4061
|
modelPreset = validatedModelPreset;
|
|
4036
4062
|
}
|
|
@@ -132,8 +132,8 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
132
132
|
* would retain the old hot polling loop, so replacement is required.
|
|
133
133
|
*/
|
|
134
134
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
135
|
-
discord:
|
|
136
|
-
slack:
|
|
135
|
+
discord: 71,
|
|
136
|
+
slack: 74,
|
|
137
137
|
};
|
|
138
138
|
|
|
139
139
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -114,6 +114,10 @@ function readGeneration(value: unknown): number | undefined {
|
|
|
114
114
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function readSequence(value: unknown): number | undefined {
|
|
118
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
117
121
|
/**
|
|
118
122
|
* What one representation of a frame states about an identity it owns.
|
|
119
123
|
*
|
|
@@ -138,7 +142,7 @@ const ABSENT_IDENTITY: IdentityClaim<never> = { state: "absent" };
|
|
|
138
142
|
*/
|
|
139
143
|
function identityClaim<T>(
|
|
140
144
|
frame: Record<string, unknown> | undefined,
|
|
141
|
-
key: "sessionId" | "generation" | "name" | "kind",
|
|
145
|
+
key: "sessionId" | "generation" | "name" | "kind" | "seq",
|
|
142
146
|
read: (value: unknown) => T | undefined,
|
|
143
147
|
): IdentityClaim<T> {
|
|
144
148
|
if (!frame || !Object.hasOwn(frame, key)) return ABSENT_IDENTITY;
|
|
@@ -232,11 +236,17 @@ function correlateFrame(frame: Record<string, unknown>): CorrelatedFrame | undef
|
|
|
232
236
|
identityClaim(payload, "generation", readGeneration),
|
|
233
237
|
);
|
|
234
238
|
if (!generation.ok) return undefined;
|
|
239
|
+
const seq = reconcileIdentity(
|
|
240
|
+
identityClaim(frame, "seq", readSequence),
|
|
241
|
+
identityClaim(payload, "seq", readSequence),
|
|
242
|
+
);
|
|
243
|
+
if (!seq.ok) return undefined;
|
|
235
244
|
return {
|
|
236
245
|
body,
|
|
237
246
|
name: envelopeName.value ?? bodyName,
|
|
238
247
|
sessionId: sessionId.value,
|
|
239
248
|
generation: generation.value,
|
|
249
|
+
seq: seq.value,
|
|
240
250
|
};
|
|
241
251
|
}
|
|
242
252
|
|