@gajae-code/coding-agent 0.15.5 → 0.16.0
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 +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -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/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -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 +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -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/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.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 +72 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -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/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -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 +67 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- 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 +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- 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/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -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/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -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 +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- 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 +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- 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 +2795 -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 +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- 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/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- 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 +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
- package/src/sdk/bus/index.ts +51 -4
- 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/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- 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 +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- 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 +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- 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 +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- 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 +1268 -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 +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -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
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
type Api,
|
|
5
5
|
type AuthCredentialSelector,
|
|
6
6
|
type CredentialRemovalTarget,
|
|
7
|
+
isSqliteCorruptionError,
|
|
8
|
+
isSqliteError,
|
|
7
9
|
type Model,
|
|
8
10
|
resolveOAuthStorageProvider,
|
|
9
11
|
} from "@gajae-code/ai/core";
|
|
@@ -106,6 +108,7 @@ import { TelegramDaemonController } from "../../sdk/bus/telegram-daemon-control"
|
|
|
106
108
|
import { runTelegramSetup, type TelegramSetupPreflight } from "../../sdk/bus/telegram-setup";
|
|
107
109
|
import { clearPersistentPinForRemovedRows } from "../../session/account-inventory";
|
|
108
110
|
import type { DefaultFallbackRuntimeState } from "../../session/agent-session";
|
|
111
|
+
import { CREDENTIAL_STORE_UNREADABLE_MESSAGE } from "../../session/credential-store-errors";
|
|
109
112
|
import { type SessionInfo, SessionManager } from "../../session/session-manager";
|
|
110
113
|
import { getTreeForInternalRead } from "../../session/session-manager-internal";
|
|
111
114
|
import { FileSessionStorage } from "../../session/session-storage";
|
|
@@ -125,6 +128,12 @@ import {
|
|
|
125
128
|
logCredentialAutoImportFailures,
|
|
126
129
|
runExternalCredentialAutoImport,
|
|
127
130
|
} from "../../setup/credential-auto-import";
|
|
131
|
+
|
|
132
|
+
function credentialStoreOperationError(action: "Login" | "Logout", error: unknown): string {
|
|
133
|
+
const code = error && typeof error === "object" ? (error as { code?: unknown }).code : undefined;
|
|
134
|
+
return `${action} failed: credential store operation failed${typeof code === "string" ? ` (${code})` : ""}.`;
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
import {
|
|
129
138
|
filterAutoImportOAuthCredentials,
|
|
130
139
|
formatDiscoverySummary,
|
|
@@ -172,6 +181,7 @@ import {
|
|
|
172
181
|
FrictionlessOnboardingSelectorComponent,
|
|
173
182
|
type FrictionlessOnboardingStage,
|
|
174
183
|
getFrictionlessOnboardingCopy,
|
|
184
|
+
InterfaceLanguageSelectorComponent,
|
|
175
185
|
} from "../components/frictionless-onboarding-selector";
|
|
176
186
|
import type { PetMode } from "../components/gajae-pet-widget";
|
|
177
187
|
import { HistorySearchComponent } from "../components/history-search";
|
|
@@ -216,6 +226,7 @@ import type { SessionObserverRegistry } from "../session-observer-registry";
|
|
|
216
226
|
import { buildOAuthLoginAnchor, createOAuthUrlCopyLease } from "../shared/oauth-url-copy";
|
|
217
227
|
import type { TasksAggregator } from "../tasks-aggregator";
|
|
218
228
|
import type { TranscriptItemRegistry } from "../transcript-item-registry";
|
|
229
|
+
import { resolveExplicitUiLanguage, type UiLanguage } from "../ui-language";
|
|
219
230
|
import { acquireResumeProgressLease, type ResumeProgressLease } from "../utils/ui-helpers";
|
|
220
231
|
|
|
221
232
|
const CALLBACK_SERVER_PROVIDERS = new Set<string>([
|
|
@@ -1366,11 +1377,16 @@ export class SelectorController {
|
|
|
1366
1377
|
async #refreshOAuthProviderAuthState(): Promise<void> {
|
|
1367
1378
|
const oauthProviders = getOAuthProviders();
|
|
1368
1379
|
await Promise.all(
|
|
1369
|
-
oauthProviders.map(provider =>
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1380
|
+
oauthProviders.map(async provider => {
|
|
1381
|
+
try {
|
|
1382
|
+
await this.ctx.session.modelRegistry.getApiKeyForProvider(
|
|
1383
|
+
provider.id,
|
|
1384
|
+
this.ctx.session.credentialSessionId,
|
|
1385
|
+
);
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
if (isSqliteError(error)) throw error;
|
|
1388
|
+
}
|
|
1389
|
+
}),
|
|
1374
1390
|
);
|
|
1375
1391
|
}
|
|
1376
1392
|
/**
|
|
@@ -1469,11 +1485,57 @@ export class SelectorController {
|
|
|
1469
1485
|
|
|
1470
1486
|
async showFrictionlessOnboarding(): Promise<void> {
|
|
1471
1487
|
const agentDir = this.ctx.session.getSessionAgentDir();
|
|
1488
|
+
const languagePreference = resolveExplicitUiLanguage(
|
|
1489
|
+
this.ctx.settings.has("ui.language") ? this.ctx.settings.get("ui.language") : undefined,
|
|
1490
|
+
process.env.GJC_UI_LANGUAGE,
|
|
1491
|
+
);
|
|
1492
|
+
let selectedLanguage: UiLanguage = languagePreference.language;
|
|
1493
|
+
if (!languagePreference.hasPreference && this.ctx.settings.canPersistDurableConfig()) {
|
|
1494
|
+
const decision = Promise.withResolvers<UiLanguage | undefined>();
|
|
1495
|
+
let settled = false;
|
|
1496
|
+
let unregisterStop: (() => void) | undefined;
|
|
1497
|
+
const finish = (language: UiLanguage | undefined): void => {
|
|
1498
|
+
if (settled) return;
|
|
1499
|
+
settled = true;
|
|
1500
|
+
unregisterStop?.();
|
|
1501
|
+
decision.resolve(language);
|
|
1502
|
+
};
|
|
1503
|
+
this.showSelector(done => {
|
|
1504
|
+
unregisterStop = this.ctx.onStop(() => {
|
|
1505
|
+
done();
|
|
1506
|
+
finish(undefined);
|
|
1507
|
+
});
|
|
1508
|
+
const selector = new InterfaceLanguageSelectorComponent(
|
|
1509
|
+
language => {
|
|
1510
|
+
done();
|
|
1511
|
+
finish(language);
|
|
1512
|
+
},
|
|
1513
|
+
() => {
|
|
1514
|
+
done();
|
|
1515
|
+
finish(undefined);
|
|
1516
|
+
},
|
|
1517
|
+
);
|
|
1518
|
+
return { component: selector, focus: selector };
|
|
1519
|
+
});
|
|
1520
|
+
selectedLanguage = (await decision.promise) ?? "en";
|
|
1521
|
+
try {
|
|
1522
|
+
await this.ctx.settings.commitAtomicBatch([{ path: "ui.language", op: "set", value: selectedLanguage }]);
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
this.ctx.showError(
|
|
1525
|
+
error instanceof Error
|
|
1526
|
+
? `${error.message}; continuing in English.`
|
|
1527
|
+
: "Cannot save the interface language; continuing in English.",
|
|
1528
|
+
);
|
|
1529
|
+
selectedLanguage = "en";
|
|
1530
|
+
}
|
|
1531
|
+
} else if (!languagePreference.hasPreference) {
|
|
1532
|
+
this.ctx.showError("Cannot save the interface language; continuing in English.");
|
|
1533
|
+
}
|
|
1472
1534
|
const presence = await discoverOnboardingRootPresence();
|
|
1473
1535
|
// An explicit `/language` (or settings) selection outranks locale and transcript evidence.
|
|
1474
1536
|
const profileOptions = {
|
|
1475
1537
|
osLocale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
1476
|
-
|
|
1538
|
+
preferredLanguage: selectedLanguage,
|
|
1477
1539
|
};
|
|
1478
1540
|
const initialProfile = deriveOnboardingProfile([], profileOptions);
|
|
1479
1541
|
|
|
@@ -3557,7 +3619,8 @@ export class SelectorController {
|
|
|
3557
3619
|
}
|
|
3558
3620
|
this.ctx.ui.requestRender();
|
|
3559
3621
|
} catch (error: unknown) {
|
|
3560
|
-
this.ctx.showError(
|
|
3622
|
+
if (isSqliteCorruptionError(error)) this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
|
|
3623
|
+
else this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3561
3624
|
} finally {
|
|
3562
3625
|
oauthUrlCopyLease.release();
|
|
3563
3626
|
if (useManualInput) {
|
|
@@ -3599,7 +3662,8 @@ export class SelectorController {
|
|
|
3599
3662
|
`Successfully removed ${result.ids.length} stored credential${result.ids.length === 1 ? "" : "s"} from ${providerId}.`,
|
|
3600
3663
|
);
|
|
3601
3664
|
} catch (error: unknown) {
|
|
3602
|
-
this.ctx.showError(
|
|
3665
|
+
if (isSqliteCorruptionError(error)) this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
|
|
3666
|
+
else this.ctx.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3603
3667
|
}
|
|
3604
3668
|
}
|
|
3605
3669
|
|
|
@@ -3616,75 +3680,90 @@ export class SelectorController {
|
|
|
3616
3680
|
return;
|
|
3617
3681
|
}
|
|
3618
3682
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
done();
|
|
3639
|
-
this.ctx.ui.requestRender();
|
|
3640
|
-
},
|
|
3641
|
-
{
|
|
3642
|
-
accountProviderId: selectedProviderId,
|
|
3643
|
-
onAccountSelect: async (selectorValue: AuthCredentialSelector) => {
|
|
3644
|
-
await this.ctx.session.setCredentialPin(selectedProviderId, selectorValue);
|
|
3645
|
-
selector.stopValidation();
|
|
3646
|
-
done();
|
|
3647
|
-
this.ctx.showStatus(`Pinned OAuth account for ${selectedProviderId} to this session.`);
|
|
3648
|
-
},
|
|
3649
|
-
onAutoSelect: async () => {
|
|
3650
|
-
await this.ctx.session.setCredentialAuto(selectedProviderId);
|
|
3651
|
-
selector.stopValidation();
|
|
3652
|
-
done();
|
|
3653
|
-
this.ctx.showStatus(`Using AUTO (ranked) OAuth accounts for ${selectedProviderId}.`);
|
|
3654
|
-
},
|
|
3655
|
-
onAddAccount: async () => {
|
|
3683
|
+
try {
|
|
3684
|
+
const hasOAuthInventory = authStorage
|
|
3685
|
+
.listCredentialInventory(selectedProviderId)
|
|
3686
|
+
.some(row => row.provider === selectedProviderId && row.credentialKind === "oauth");
|
|
3687
|
+
const hasRemovableCredential = authStorage
|
|
3688
|
+
.listCredentialRemovalTargets(selectedProviderId)
|
|
3689
|
+
.some(target => target.provider === selectedProviderId);
|
|
3690
|
+
if (mode === "logout" && hasOAuthInventory && !hasRemovableCredential) {
|
|
3691
|
+
await this.#handleLogout(selectedProviderId);
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
if (hasOAuthInventory && !options?.manualCode) {
|
|
3695
|
+
this.showSelector(done => {
|
|
3696
|
+
let selector: OAuthSelectorComponent;
|
|
3697
|
+
selector = new OAuthSelectorComponent(
|
|
3698
|
+
mode,
|
|
3699
|
+
authStorage,
|
|
3700
|
+
() => undefined,
|
|
3701
|
+
() => {
|
|
3656
3702
|
selector.stopValidation();
|
|
3657
3703
|
done();
|
|
3658
|
-
|
|
3704
|
+
this.ctx.ui.requestRender();
|
|
3659
3705
|
},
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3706
|
+
{
|
|
3707
|
+
accountProviderId: selectedProviderId,
|
|
3708
|
+
onAccountSelect: async (selectorValue: AuthCredentialSelector) => {
|
|
3709
|
+
await this.ctx.session.setCredentialPin(selectedProviderId, selectorValue);
|
|
3710
|
+
selector.stopValidation();
|
|
3711
|
+
done();
|
|
3712
|
+
this.ctx.showStatus(`Pinned OAuth account for ${selectedProviderId} to this session.`);
|
|
3713
|
+
},
|
|
3714
|
+
onAutoSelect: async () => {
|
|
3715
|
+
await this.ctx.session.setCredentialAuto(selectedProviderId);
|
|
3716
|
+
selector.stopValidation();
|
|
3717
|
+
done();
|
|
3718
|
+
this.ctx.showStatus(`Using AUTO (ranked) OAuth accounts for ${selectedProviderId}.`);
|
|
3719
|
+
},
|
|
3720
|
+
onAddAccount: async () => {
|
|
3721
|
+
selector.stopValidation();
|
|
3722
|
+
done();
|
|
3723
|
+
await this.#handleOAuthLogin(selectedProviderId, options);
|
|
3724
|
+
},
|
|
3725
|
+
onAccountRemove: async targets => {
|
|
3726
|
+
await this.#handleLogout(selectedProviderId, targets);
|
|
3727
|
+
selector.stopValidation();
|
|
3728
|
+
done();
|
|
3729
|
+
},
|
|
3664
3730
|
},
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3731
|
+
);
|
|
3732
|
+
return { component: selector, focus: selector };
|
|
3733
|
+
});
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
if (mode === "login") {
|
|
3737
|
+
await this.#handleOAuthLogin(selectedProviderId, options);
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
// mode === "logout" with no OAuth accounts: remove stored api-key credentials directly.
|
|
3741
|
+
await this.#handleLogout(selectedProviderId);
|
|
3669
3742
|
return;
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
|
-
|
|
3743
|
+
} catch (error: unknown) {
|
|
3744
|
+
if (isSqliteCorruptionError(error)) this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
|
|
3745
|
+
else if (isSqliteError(error))
|
|
3746
|
+
this.ctx.showError(credentialStoreOperationError(mode === "login" ? "Login" : "Logout", error));
|
|
3747
|
+
else throw error;
|
|
3673
3748
|
return;
|
|
3674
3749
|
}
|
|
3675
|
-
// mode === "logout" with no OAuth accounts: remove stored api-key credentials directly.
|
|
3676
|
-
await this.#handleLogout(selectedProviderId);
|
|
3677
|
-
return;
|
|
3678
3750
|
}
|
|
3679
3751
|
|
|
3680
3752
|
if (mode === "logout") {
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3753
|
+
try {
|
|
3754
|
+
await this.#refreshOAuthProviderAuthState();
|
|
3755
|
+
const oauthProviders = getOAuthProviders();
|
|
3756
|
+
const loggedInProviders = oauthProviders.filter(provider =>
|
|
3757
|
+
this.ctx.session.modelRegistry.authStorage.hasAuth(provider.id),
|
|
3758
|
+
);
|
|
3759
|
+
if (loggedInProviders.length === 0) {
|
|
3760
|
+
this.ctx.showStatus("No OAuth providers logged in. Use /login first.");
|
|
3761
|
+
return;
|
|
3762
|
+
}
|
|
3763
|
+
} catch (error: unknown) {
|
|
3764
|
+
if (isSqliteCorruptionError(error)) this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
|
|
3765
|
+
else if (isSqliteError(error)) this.ctx.showError(credentialStoreOperationError("Logout", error));
|
|
3766
|
+
else throw error;
|
|
3688
3767
|
return;
|
|
3689
3768
|
}
|
|
3690
3769
|
}
|
|
@@ -3807,37 +3886,53 @@ export class SelectorController {
|
|
|
3807
3886
|
}
|
|
3808
3887
|
}
|
|
3809
3888
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
() => {
|
|
3821
|
-
selector.stopValidation();
|
|
3822
|
-
done();
|
|
3823
|
-
this.ctx.ui.requestLayoutRender("oauth-selector-close");
|
|
3824
|
-
},
|
|
3825
|
-
{
|
|
3826
|
-
validateAuth: async (selectedProviderId: string) => {
|
|
3827
|
-
const apiKey = await this.ctx.session.modelRegistry.getApiKeyForProvider(
|
|
3828
|
-
selectedProviderId,
|
|
3829
|
-
this.ctx.session.credentialSessionId,
|
|
3830
|
-
);
|
|
3831
|
-
return !!apiKey;
|
|
3889
|
+
try {
|
|
3890
|
+
this.showSelector(done => {
|
|
3891
|
+
let selector: OAuthSelectorComponent;
|
|
3892
|
+
selector = new OAuthSelectorComponent(
|
|
3893
|
+
mode,
|
|
3894
|
+
this.ctx.session.modelRegistry.authStorage,
|
|
3895
|
+
async (selectedProviderId: string) => {
|
|
3896
|
+
selector.stopValidation();
|
|
3897
|
+
done();
|
|
3898
|
+
await this.showOAuthSelector(mode, selectedProviderId);
|
|
3832
3899
|
},
|
|
3833
|
-
|
|
3834
|
-
|
|
3900
|
+
() => {
|
|
3901
|
+
selector.stopValidation();
|
|
3902
|
+
done();
|
|
3903
|
+
this.ctx.ui.requestLayoutRender("oauth-selector-close");
|
|
3835
3904
|
},
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3905
|
+
{
|
|
3906
|
+
validateAuth: async (selectedProviderId: string) => {
|
|
3907
|
+
const apiKey = await this.ctx.session.modelRegistry.getApiKeyForProvider(
|
|
3908
|
+
selectedProviderId,
|
|
3909
|
+
this.ctx.session.credentialSessionId,
|
|
3910
|
+
);
|
|
3911
|
+
return !!apiKey;
|
|
3912
|
+
},
|
|
3913
|
+
onValidationError: error => {
|
|
3914
|
+
if (!isSqliteError(error)) return false;
|
|
3915
|
+
selector.stopValidation();
|
|
3916
|
+
done();
|
|
3917
|
+
this.ctx.showError(
|
|
3918
|
+
isSqliteCorruptionError(error)
|
|
3919
|
+
? CREDENTIAL_STORE_UNREADABLE_MESSAGE
|
|
3920
|
+
: credentialStoreOperationError("Login", error),
|
|
3921
|
+
);
|
|
3922
|
+
return true;
|
|
3923
|
+
},
|
|
3924
|
+
requestRender: () => {
|
|
3925
|
+
this.ctx.ui.requestLayoutRender("oauth-selector-spinner");
|
|
3926
|
+
},
|
|
3927
|
+
externalCredentialCandidates,
|
|
3928
|
+
},
|
|
3929
|
+
);
|
|
3930
|
+
return { component: selector, focus: selector };
|
|
3931
|
+
});
|
|
3932
|
+
} catch (error: unknown) {
|
|
3933
|
+
if (!isSqliteCorruptionError(error)) throw error;
|
|
3934
|
+
this.ctx.showError(CREDENTIAL_STORE_UNREADABLE_MESSAGE);
|
|
3935
|
+
}
|
|
3841
3936
|
}
|
|
3842
3937
|
|
|
3843
3938
|
showDebugSelector(): void {
|
|
@@ -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;
|