@gajae-code/coding-agent 0.12.7 → 0.12.10
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 +48 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +23 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/discovery/agents-md.d.ts +11 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +54 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +19 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +3 -2
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -1
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +215 -99
- package/src/commands/notify.ts +19 -8
- package/src/commands/sdk.ts +54 -0
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +866 -76
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +525 -93
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +571 -137
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +34 -14
- package/src/session/agent-session.ts +109 -37
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +418 -47
- package/src/session/internal/native-publish-outcome.ts +15 -3
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +117 -15
- package/src/system-prompt.ts +28 -15
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +4 -2
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/tools/telegram-send.ts +4 -2
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
|
@@ -16,6 +16,8 @@ export type NativePublishOperation = "direct_rename" | "retained_file" | "retain
|
|
|
16
16
|
|
|
17
17
|
export type NativePublishPrimitive =
|
|
18
18
|
| "renameat2_noreplace"
|
|
19
|
+
| "linkat_noreplace"
|
|
20
|
+
| "mkdirat_renameat_noreplace"
|
|
19
21
|
| "renameatx_np_excl"
|
|
20
22
|
| "windows_rename_noreplace"
|
|
21
23
|
| "unsupported"
|
|
@@ -24,6 +26,7 @@ export type NativePublishPhase =
|
|
|
24
26
|
| "preflight"
|
|
25
27
|
| "file_sync"
|
|
26
28
|
| "rename"
|
|
29
|
+
| "source_unlink"
|
|
27
30
|
| "source_parent_sync"
|
|
28
31
|
| "destination_parent_sync"
|
|
29
32
|
| "terminal_identity"
|
|
@@ -48,6 +51,7 @@ export type NativePublishIdentity = {
|
|
|
48
51
|
readonly dev: string;
|
|
49
52
|
readonly ino: string;
|
|
50
53
|
readonly size: string;
|
|
54
|
+
readonly nlink?: string;
|
|
51
55
|
readonly mtimeNs: string;
|
|
52
56
|
readonly ctimeNs: string;
|
|
53
57
|
readonly sha256?: string;
|
|
@@ -82,6 +86,8 @@ const reasons = new Set<NativePublishReason>([
|
|
|
82
86
|
]);
|
|
83
87
|
const primitives = new Set<NativePublishPrimitive>([
|
|
84
88
|
"renameat2_noreplace",
|
|
89
|
+
"linkat_noreplace",
|
|
90
|
+
"mkdirat_renameat_noreplace",
|
|
85
91
|
"renameatx_np_excl",
|
|
86
92
|
"windows_rename_noreplace",
|
|
87
93
|
"unsupported",
|
|
@@ -91,6 +97,7 @@ const phases = new Set<NativePublishPhase>([
|
|
|
91
97
|
"preflight",
|
|
92
98
|
"file_sync",
|
|
93
99
|
"rename",
|
|
100
|
+
"source_unlink",
|
|
94
101
|
"source_parent_sync",
|
|
95
102
|
"destination_parent_sync",
|
|
96
103
|
"terminal_identity",
|
|
@@ -126,12 +133,13 @@ const malformed: NativePublishOutcome = Object.freeze({
|
|
|
126
133
|
|
|
127
134
|
function validIdentity(value: unknown): boolean {
|
|
128
135
|
if (value === undefined) return true;
|
|
129
|
-
if (!ownPlainRecord(value) || !exactKeys(value, ["dev", "ino", "size", "mtimeNs", "ctimeNs", "sha256"]))
|
|
136
|
+
if (!ownPlainRecord(value) || !exactKeys(value, ["dev", "ino", "nlink", "size", "mtimeNs", "ctimeNs", "sha256"]))
|
|
130
137
|
return false;
|
|
131
138
|
const decimal = (field: unknown) => typeof field === "string" && /^-?[0-9]{1,32}$/.test(field);
|
|
132
139
|
return (
|
|
133
140
|
decimal(value.dev) &&
|
|
134
141
|
decimal(value.ino) &&
|
|
142
|
+
(value.nlink === undefined || decimal(value.nlink)) &&
|
|
135
143
|
decimal(value.size) &&
|
|
136
144
|
decimal(value.mtimeNs) &&
|
|
137
145
|
decimal(value.ctimeNs) &&
|
|
@@ -202,7 +210,7 @@ function legalOutcome(outcome: NativePublishOutcome): boolean {
|
|
|
202
210
|
((outcome.reason === "durability_not_provable" &&
|
|
203
211
|
["file_sync", "source_parent_sync", "destination_parent_sync"].includes(outcome.phase)) ||
|
|
204
212
|
(outcome.reason === "identity_violation" && outcome.phase === "terminal_identity") ||
|
|
205
|
-
(outcome.reason === "io_failure" &&
|
|
213
|
+
(outcome.reason === "io_failure" && ["source_unlink", "terminal_identity"].includes(outcome.phase)))
|
|
206
214
|
);
|
|
207
215
|
}
|
|
208
216
|
|
|
@@ -245,8 +253,12 @@ export function classifyNativePublishOutcome(
|
|
|
245
253
|
const outcome = value as unknown as NativePublishOutcome;
|
|
246
254
|
if (!legalOutcome(outcome)) return malformed;
|
|
247
255
|
if (operation === "direct_rename") return outcome;
|
|
256
|
+
const retainedPrimitiveValid =
|
|
257
|
+
operation === "retained_file"
|
|
258
|
+
? outcome.primitive === "renameat2_noreplace" || outcome.primitive === "linkat_noreplace"
|
|
259
|
+
: outcome.primitive === "renameat2_noreplace" || outcome.primitive === "mkdirat_renameat_noreplace";
|
|
248
260
|
if (
|
|
249
|
-
|
|
261
|
+
!retainedPrimitiveValid ||
|
|
250
262
|
(outcome.ok && (!outcome.identity || outcome.durabilityState !== "proven" || outcome.phase !== "complete"))
|
|
251
263
|
)
|
|
252
264
|
return malformed;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const SESSION_MIGRATION_BUSY_MESSAGE =
|
|
2
|
+
"Another session migration is still active. Wait for it to finish, then retry.";
|
|
3
|
+
|
|
4
|
+
export class SessionMigrationBusyError extends Error {
|
|
5
|
+
readonly code = "migration_busy";
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
super(SESSION_MIGRATION_BUSY_MESSAGE);
|
|
9
|
+
this.name = "SessionMigrationBusyError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -83,8 +83,8 @@ import {
|
|
|
83
83
|
mayCleanManagedTreeStaging,
|
|
84
84
|
retainManagedDirectoryAuthority,
|
|
85
85
|
} from "./internal/managed-session-storage";
|
|
86
|
-
|
|
87
86
|
import { classifyNativePublishOutcome, formatNativePublishDiagnostic } from "./internal/native-publish-outcome";
|
|
87
|
+
import { SessionMigrationBusyError } from "./internal/session-open-errors";
|
|
88
88
|
import {
|
|
89
89
|
hasOnlyKeys as hasOnlyMemoryGuardKeys,
|
|
90
90
|
isMemoryGuardDecimalString,
|
|
@@ -940,6 +940,8 @@ export class SessionMigrationPolicyError extends Error {
|
|
|
940
940
|
}
|
|
941
941
|
|
|
942
942
|
export class SessionArtifactCapacityError extends Error {
|
|
943
|
+
readonly code = "artifact_capacity_exceeded";
|
|
944
|
+
|
|
943
945
|
constructor(message: string) {
|
|
944
946
|
super(message);
|
|
945
947
|
this.name = "SessionArtifactCapacityError";
|
|
@@ -954,7 +956,12 @@ export interface StrictSessionOpenSuccess {
|
|
|
954
956
|
|
|
955
957
|
export interface StrictSessionOpenFailure {
|
|
956
958
|
kind: "error";
|
|
957
|
-
reason:
|
|
959
|
+
reason:
|
|
960
|
+
| ResumeTailError["reason"]
|
|
961
|
+
| "identity-mismatch"
|
|
962
|
+
| "migration-required"
|
|
963
|
+
| "artifact_capacity_exceeded"
|
|
964
|
+
| "migration_busy";
|
|
958
965
|
message?: string;
|
|
959
966
|
}
|
|
960
967
|
|
|
@@ -4707,6 +4714,7 @@ export const MATERIALIZED_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
|
4707
4714
|
export const SessionManagerTestHooks: {
|
|
4708
4715
|
materializedCacheMaxBytesOverride?: number;
|
|
4709
4716
|
beforeResidentTransitionIndexBuild?: () => void;
|
|
4717
|
+
afterForkSnapshot?: () => void | Promise<void>;
|
|
4710
4718
|
} = {};
|
|
4711
4719
|
|
|
4712
4720
|
function materializedCacheMaxBytes(): number {
|
|
@@ -6165,6 +6173,7 @@ export class SessionManager {
|
|
|
6165
6173
|
const candidate = listing.owned.find(candidate => path.resolve(candidate.path) === requestedPath);
|
|
6166
6174
|
if (!candidate) throw new Error("Managed session deletion requires exact logical authorization.");
|
|
6167
6175
|
const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
|
|
6176
|
+
if (deleted.kind === "error" && deleted.code === "migration_busy") throw new SessionMigrationBusyError();
|
|
6168
6177
|
if (deleted.kind !== "deleted" && deleted.kind !== "already_deleted")
|
|
6169
6178
|
throw new Error(`Could not delete managed session: ${deleted.message}`);
|
|
6170
6179
|
} else {
|
|
@@ -6248,6 +6257,8 @@ export class SessionManager {
|
|
|
6248
6257
|
newHeader,
|
|
6249
6258
|
...materializedEntries.filter((entry): entry is SessionEntry => entry.type !== "session"),
|
|
6250
6259
|
];
|
|
6260
|
+
const forkSnapshotHook = SessionManagerTestHooks.afterForkSnapshot;
|
|
6261
|
+
if (forkSnapshotHook) await forkSnapshotHook();
|
|
6251
6262
|
|
|
6252
6263
|
await this.#closePersistWriter();
|
|
6253
6264
|
this.#persistChain = Promise.resolve();
|
|
@@ -9603,6 +9614,7 @@ export class SessionManager {
|
|
|
9603
9614
|
authority,
|
|
9604
9615
|
);
|
|
9605
9616
|
} catch (error) {
|
|
9617
|
+
if (error instanceof Error && error.message === "migration_busy") throw new SessionMigrationBusyError();
|
|
9606
9618
|
if (error instanceof Error && error.message.startsWith("Managed root authority changed"))
|
|
9607
9619
|
managedDestinationStore.assertBound();
|
|
9608
9620
|
throw error;
|
|
@@ -9611,6 +9623,7 @@ export class SessionManager {
|
|
|
9611
9623
|
managedDestinationStore.assertBound();
|
|
9612
9624
|
if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
|
|
9613
9625
|
if (opened.code === "artifact_capacity_exceeded") throw new SessionArtifactCapacityError(opened.message);
|
|
9626
|
+
if (opened.code === "migration_busy") throw new SessionMigrationBusyError();
|
|
9614
9627
|
throw new Error(`Could not open managed session: ${opened.message}`);
|
|
9615
9628
|
}
|
|
9616
9629
|
assertManagedDestinationBound();
|
|
@@ -9706,7 +9719,15 @@ export class SessionManager {
|
|
|
9706
9719
|
throw new Error(`Could not open session: ${inspected.reason}`);
|
|
9707
9720
|
}
|
|
9708
9721
|
const opened = await SessionManager.openExistingStrict(inspected.identity, destination, storage, migrationPolicy);
|
|
9709
|
-
if (opened.kind === "error")
|
|
9722
|
+
if (opened.kind === "error") {
|
|
9723
|
+
if (opened.reason === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
|
|
9724
|
+
if (opened.reason === "artifact_capacity_exceeded")
|
|
9725
|
+
throw new SessionArtifactCapacityError(
|
|
9726
|
+
opened.message ?? "Session artifacts exceed the migration capacity.",
|
|
9727
|
+
);
|
|
9728
|
+
if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
|
|
9729
|
+
throw new Error(`Could not open session: ${opened.reason}`);
|
|
9730
|
+
}
|
|
9710
9731
|
return opened.manager;
|
|
9711
9732
|
}
|
|
9712
9733
|
|
|
@@ -9844,6 +9865,7 @@ export class SessionManager {
|
|
|
9844
9865
|
const candidate = listing.owned.find(item => path.resolve(item.path) === path.resolve(sessionPath));
|
|
9845
9866
|
if (!candidate) throw new Error("Session is not an authorized managed candidate.");
|
|
9846
9867
|
const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
|
|
9868
|
+
if (deleted.kind === "error" && deleted.code === "migration_busy") throw new SessionMigrationBusyError();
|
|
9847
9869
|
if (deleted.kind !== "deleted" && deleted.kind !== "already_deleted")
|
|
9848
9870
|
throw new Error(`Could not delete managed session: ${deleted.message}`);
|
|
9849
9871
|
}
|
|
@@ -10389,6 +10411,7 @@ export class SessionManager {
|
|
|
10389
10411
|
return { kind: "error", reason: "legacy_migration_disabled" };
|
|
10390
10412
|
if (error instanceof SessionArtifactCapacityError)
|
|
10391
10413
|
return { kind: "error", reason: "artifact_capacity_exceeded", message: error.message };
|
|
10414
|
+
if (error instanceof SessionMigrationBusyError) return { kind: "error", reason: "migration_busy" };
|
|
10392
10415
|
if (
|
|
10393
10416
|
error instanceof Error &&
|
|
10394
10417
|
(error.message.includes("source_changed") || error.message.includes("changed before migration"))
|
|
@@ -10451,6 +10474,7 @@ export class SessionManager {
|
|
|
10451
10474
|
throw new SessionArtifactCapacityError(
|
|
10452
10475
|
opened.message ?? "Session artifacts exceed the migration capacity.",
|
|
10453
10476
|
);
|
|
10477
|
+
if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
|
|
10454
10478
|
return undefined;
|
|
10455
10479
|
}
|
|
10456
10480
|
return opened.manager;
|
|
@@ -312,6 +312,7 @@ export type VerifiedSessionDeleteResult =
|
|
|
312
312
|
artifactsIdentity: SessionStorageFileIdentity | undefined;
|
|
313
313
|
/** Identity-bound quarantine path retained when recursive cleanup failed. */
|
|
314
314
|
detachedArtifactsPath: string;
|
|
315
|
+
artifactsPayloadDurable?: true;
|
|
315
316
|
/** Native snapshot required for an identity-bound recursive retry. */
|
|
316
317
|
artifactsTree: NativeDirectoryTreeSnapshot;
|
|
317
318
|
/** Transcript identity (unchanged) for retry binding. */
|
|
@@ -328,6 +329,7 @@ export type VerifiedSessionDeleteResult =
|
|
|
328
329
|
transcriptIdentity: SessionStorageFileIdentity;
|
|
329
330
|
/** Optional identity-bound transcript quarantine path for restart cleanup. */
|
|
330
331
|
detachedTranscriptPath?: string;
|
|
332
|
+
transcriptPayloadDurable?: true;
|
|
331
333
|
retainedSuccessorPath?: string;
|
|
332
334
|
retainedPlaceholderPath?: string;
|
|
333
335
|
retainedUnknownPath?: string;
|
|
@@ -423,6 +425,7 @@ type NativeDirectoryTreeApi = {
|
|
|
423
425
|
function nativeDirectoryTreeApi(): NativeDirectoryTreeApi {
|
|
424
426
|
return native as unknown as NativeDirectoryTreeApi;
|
|
425
427
|
}
|
|
428
|
+
|
|
426
429
|
function snapshotDirectoryTree(pathname: string): NativeDirectoryTreeSnapshot {
|
|
427
430
|
const result = nativeDirectoryTreeApi().snapshotDirectoryTree(pathname);
|
|
428
431
|
if (!result.ok || !result.snapshot)
|
|
@@ -438,10 +441,21 @@ function retainedTreeDoesNotExpandAuthority(
|
|
|
438
441
|
): boolean {
|
|
439
442
|
if (expected.rootDev !== retained.rootDev || expected.rootIno !== retained.rootIno) return false;
|
|
440
443
|
const expectedEntries = new Map(expected.entries.map(entry => [entry.relativePath, entry]));
|
|
444
|
+
if (retained.entries.length > expected.entries.length) return false;
|
|
441
445
|
return retained.entries.every(entry => {
|
|
442
446
|
if (entry.relativePath === "") return entry.kind === "directory";
|
|
443
447
|
const authorized = expectedEntries.get(entry.relativePath);
|
|
444
|
-
|
|
448
|
+
if (
|
|
449
|
+
authorized === undefined ||
|
|
450
|
+
authorized.kind !== entry.kind ||
|
|
451
|
+
authorized.dev !== entry.dev ||
|
|
452
|
+
authorized.ino !== entry.ino ||
|
|
453
|
+
authorized.nlink !== entry.nlink
|
|
454
|
+
)
|
|
455
|
+
return false;
|
|
456
|
+
if (entry.kind !== "file") return entry.size === authorized.size;
|
|
457
|
+
const scrubbed = entry.size === "0" && entry.sha256 === createHash("sha256").update("").digest("hex");
|
|
458
|
+
return scrubbed || (entry.size === authorized.size && entry.sha256 === authorized.sha256);
|
|
445
459
|
});
|
|
446
460
|
}
|
|
447
461
|
|
|
@@ -938,7 +952,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
938
952
|
"Transcript parent identity does not match authorization",
|
|
939
953
|
);
|
|
940
954
|
const authorizedTranscriptParentIdentity = transcriptParentIdentity ?? parentIdentity;
|
|
941
|
-
if (detachedArtifactsPath) {
|
|
955
|
+
if (detachedArtifactsPath && !artifactsRemoved) {
|
|
942
956
|
if (
|
|
943
957
|
!expectedArtifactsIdentity ||
|
|
944
958
|
path.dirname(detachedArtifactsPath) !== path.dirname(transcriptPath) ||
|
|
@@ -997,6 +1011,9 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
997
1011
|
artifactsIdentity: expectedArtifactsIdentity,
|
|
998
1012
|
detachedArtifactsPath: retainedRoot,
|
|
999
1013
|
artifactsTree: retainedTree,
|
|
1014
|
+
...((removal as typeof removal & { payloadDurable?: boolean }).payloadDurable === true
|
|
1015
|
+
? { artifactsPayloadDurable: true as const }
|
|
1016
|
+
: {}),
|
|
1000
1017
|
...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
|
|
1001
1018
|
? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
|
|
1002
1019
|
: {}),
|
|
@@ -1045,6 +1062,24 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1045
1062
|
"Artifact path reappeared after durable artifact-phase completion",
|
|
1046
1063
|
);
|
|
1047
1064
|
}
|
|
1065
|
+
if (artifactsRemoved && detachedArtifactsPath && expectedArtifactsIdentity && expectedArtifactsTree) {
|
|
1066
|
+
const retainedIdentity = this.#optionalDirectoryIdentity(detachedArtifactsPath);
|
|
1067
|
+
if (
|
|
1068
|
+
!retainedIdentity ||
|
|
1069
|
+
retainedIdentity.dev !== expectedArtifactsIdentity.dev ||
|
|
1070
|
+
retainedIdentity.ino !== expectedArtifactsIdentity.ino
|
|
1071
|
+
)
|
|
1072
|
+
throw new SessionDeleteVerificationError(
|
|
1073
|
+
"artifacts",
|
|
1074
|
+
"Retained artifact root identity changed before transcript cleanup",
|
|
1075
|
+
);
|
|
1076
|
+
const retainedTree = snapshotDirectoryTree(detachedArtifactsPath);
|
|
1077
|
+
if (!retainedTreeDoesNotExpandAuthority(expectedArtifactsTree, retainedTree))
|
|
1078
|
+
throw new SessionDeleteVerificationError(
|
|
1079
|
+
"artifacts",
|
|
1080
|
+
"Partial artifact cleanup expanded retained tree authority",
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1048
1083
|
if (!artifactsIdentity && expectedArtifactsIdentity && !detachedArtifactsPath && !artifactsRemoved) {
|
|
1049
1084
|
// Absence at the original path alone is not completion: native recursive removal
|
|
1050
1085
|
// may retain the planned root or its deterministic `.removing` final-stage root.
|
|
@@ -1116,12 +1151,22 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1116
1151
|
throw new Error("parent_changed");
|
|
1117
1152
|
fs.fsyncSync(descriptor);
|
|
1118
1153
|
} catch (error) {
|
|
1119
|
-
|
|
1154
|
+
return {
|
|
1155
|
+
kind: "cleanup_pending",
|
|
1156
|
+
phase: "artifacts",
|
|
1157
|
+
error: new SessionDeleteVerificationError("artifacts", "durability_failed", {
|
|
1158
|
+
cause: toError(error),
|
|
1159
|
+
}),
|
|
1160
|
+
artifactsIdentity,
|
|
1161
|
+
detachedArtifactsPath: detach.detachedPath,
|
|
1162
|
+
artifactsTree,
|
|
1163
|
+
transcriptIdentity,
|
|
1164
|
+
};
|
|
1120
1165
|
} finally {
|
|
1121
1166
|
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1122
1167
|
}
|
|
1123
1168
|
}
|
|
1124
|
-
if (!detach.ok) {
|
|
1169
|
+
if (!detach.ok && process.platform === "win32") {
|
|
1125
1170
|
return {
|
|
1126
1171
|
kind: "cleanup_pending",
|
|
1127
1172
|
phase: "artifacts",
|
|
@@ -1132,7 +1177,6 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1132
1177
|
...(detach.retainedSuccessorPath ? { retainedSuccessorPath: detach.retainedSuccessorPath } : {}),
|
|
1133
1178
|
...(detach.retainedPlaceholderPath ? { retainedPlaceholderPath: detach.retainedPlaceholderPath } : {}),
|
|
1134
1179
|
...(detach.retainedUnknownPath ? { retainedUnknownPath: detach.retainedUnknownPath } : {}),
|
|
1135
|
-
|
|
1136
1180
|
transcriptIdentity,
|
|
1137
1181
|
};
|
|
1138
1182
|
}
|
|
@@ -1173,6 +1217,9 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1173
1217
|
artifactsIdentity,
|
|
1174
1218
|
detachedArtifactsPath: retainedRoot,
|
|
1175
1219
|
artifactsTree: retainedTree,
|
|
1220
|
+
...((removal as typeof removal & { payloadDurable?: boolean }).payloadDurable === true
|
|
1221
|
+
? { artifactsPayloadDurable: true as const }
|
|
1222
|
+
: {}),
|
|
1176
1223
|
...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
|
|
1177
1224
|
? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
|
|
1178
1225
|
: {}),
|
|
@@ -1223,6 +1270,13 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1223
1270
|
quarantineName: path.basename(plannedTranscriptPath),
|
|
1224
1271
|
});
|
|
1225
1272
|
if (!deletion.ok) {
|
|
1273
|
+
if (
|
|
1274
|
+
deletion.code === "cleanup_pending" &&
|
|
1275
|
+
(deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true &&
|
|
1276
|
+
deletion.retainedSuccessorPath === undefined &&
|
|
1277
|
+
deletion.retainedUnknownPath === undefined
|
|
1278
|
+
)
|
|
1279
|
+
return { kind: "deleted" };
|
|
1226
1280
|
const error = exactUnlinkFailure(deletion);
|
|
1227
1281
|
const retainedAuthority =
|
|
1228
1282
|
deletion.detachedPath ||
|
|
@@ -1234,6 +1288,9 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1234
1288
|
kind: "cleanup_pending",
|
|
1235
1289
|
phase: "transcript",
|
|
1236
1290
|
error,
|
|
1291
|
+
...((deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true
|
|
1292
|
+
? { transcriptPayloadDurable: true as const }
|
|
1293
|
+
: {}),
|
|
1237
1294
|
transcriptIdentity,
|
|
1238
1295
|
detachedTranscriptPath: deletion.detachedPath ?? detachedTranscriptPath,
|
|
1239
1296
|
...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)
|
|
@@ -1288,6 +1345,13 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1288
1345
|
quarantineName: path.basename(plannedTranscriptPath),
|
|
1289
1346
|
});
|
|
1290
1347
|
if (!deletion.ok) {
|
|
1348
|
+
if (
|
|
1349
|
+
deletion.code === "cleanup_pending" &&
|
|
1350
|
+
(deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true &&
|
|
1351
|
+
deletion.retainedSuccessorPath === undefined &&
|
|
1352
|
+
deletion.retainedUnknownPath === undefined
|
|
1353
|
+
)
|
|
1354
|
+
return { kind: "deleted" };
|
|
1291
1355
|
const error = exactUnlinkFailure(deletion);
|
|
1292
1356
|
const retainedAuthority =
|
|
1293
1357
|
deletion.detachedPath ||
|
|
@@ -1299,6 +1363,9 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1299
1363
|
kind: "cleanup_pending",
|
|
1300
1364
|
phase: "transcript",
|
|
1301
1365
|
error,
|
|
1366
|
+
...((deletion as typeof deletion & { payloadDurable?: boolean }).payloadDurable === true
|
|
1367
|
+
? { transcriptPayloadDurable: true as const }
|
|
1368
|
+
: {}),
|
|
1302
1369
|
transcriptIdentity,
|
|
1303
1370
|
detachedTranscriptPath: deletion.detachedPath,
|
|
1304
1371
|
...((deletion.retainedSuccessorPath ?? retainedTranscriptSuccessorPath)
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
canApplyComposerSubmission,
|
|
30
30
|
type InteractiveModeContext,
|
|
31
31
|
} from "../modes/types";
|
|
32
|
+
import { ChatDaemonController } from "../sdk/bus/chat-daemon-control";
|
|
33
|
+
import type { NotificationProvider } from "../sdk/bus/config";
|
|
32
34
|
import {
|
|
33
35
|
buildNotificationStatusReport,
|
|
34
36
|
checkNotificationHealth,
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
recoverNotifications,
|
|
40
42
|
sendNotificationTest,
|
|
41
43
|
} from "../sdk/bus/notification-service";
|
|
44
|
+
import { TelegramDaemonController } from "../sdk/bus/telegram-daemon-control";
|
|
42
45
|
import { computeCacheMissCostSummary, formatCacheMissSummaryLines } from "../session/cache-economics";
|
|
43
46
|
import { formatModelOnboardingGuidance } from "../setup/model-onboarding-guidance";
|
|
44
47
|
import {
|
|
@@ -414,6 +417,9 @@ function modelSelectionUsage(runtime: SlashCommandRuntime, currentModelLine?: st
|
|
|
414
417
|
.join("\n\n");
|
|
415
418
|
}
|
|
416
419
|
|
|
420
|
+
/** Opt into the paste-a-code OAuth login for browsers that cannot reach this machine. */
|
|
421
|
+
const MANUAL_LOGIN_FLAG = "--manual";
|
|
422
|
+
|
|
417
423
|
const EFFORT_COMMAND_INPUT_HINT = "[inherit|off|minimal|low|medium|high|xhigh|max]";
|
|
418
424
|
const EFFORT_COMMAND_ACCEPTED_VALUES = ["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
419
425
|
|
|
@@ -498,6 +504,48 @@ function buildChangelogCommandOutput(showFull: boolean): string {
|
|
|
498
504
|
return `${title}\n\n${changelogMarkdown}${hint}`;
|
|
499
505
|
}
|
|
500
506
|
|
|
507
|
+
type NotifyServiceArgs = { provider?: NotificationProvider; probe: boolean; message?: string } | { error: string };
|
|
508
|
+
|
|
509
|
+
function isNotificationProvider(value: string): value is NotificationProvider {
|
|
510
|
+
return value === "telegram" || value === "discord" || value === "slack";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function parseNotifyServiceArgs(input: string, allowMessage: boolean): NotifyServiceArgs {
|
|
514
|
+
const tokens = input.trim().split(/\s+/).filter(Boolean);
|
|
515
|
+
let provider: NotificationProvider | undefined;
|
|
516
|
+
let probe = false;
|
|
517
|
+
const message: string[] = [];
|
|
518
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
519
|
+
const token = tokens[index]!;
|
|
520
|
+
if (token === "--probe") {
|
|
521
|
+
if (allowMessage) return { error: "--probe is valid only for /notify health." };
|
|
522
|
+
probe = true;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (token === "--provider" || token.startsWith("--provider=")) {
|
|
526
|
+
const value = token === "--provider" ? tokens[++index] : token.slice("--provider=".length);
|
|
527
|
+
if (!value || !isNotificationProvider(value)) {
|
|
528
|
+
return { error: "--provider must be telegram, discord, or slack." };
|
|
529
|
+
}
|
|
530
|
+
if (provider && provider !== value) return { error: "Conflicting notification providers were supplied." };
|
|
531
|
+
provider = value;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (!provider && message.length === 0 && isNotificationProvider(token)) {
|
|
535
|
+
provider = token;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (token.startsWith("--")) return { error: `Unknown notification option: ${token}` };
|
|
539
|
+
message.push(token);
|
|
540
|
+
}
|
|
541
|
+
if (!allowMessage && message.length > 0) return { error: "Health accepts only a provider and --probe." };
|
|
542
|
+
return {
|
|
543
|
+
...(provider ? { provider } : {}),
|
|
544
|
+
probe,
|
|
545
|
+
...(message.length > 0 ? { message: message.join(" ") } : {}),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
501
549
|
const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashCommandRuntime): SlashCommandResult => {
|
|
502
550
|
runtime.ctx.editor.setText("");
|
|
503
551
|
void runtime.ctx.shutdown();
|
|
@@ -514,8 +562,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
514
562
|
{ name: "on", description: "Enable notifications for this session" },
|
|
515
563
|
{ name: "off", description: "Disable notifications for this session" },
|
|
516
564
|
{ name: "status", description: "Show notification configuration (no secrets)" },
|
|
517
|
-
{
|
|
518
|
-
|
|
565
|
+
{
|
|
566
|
+
name: "health",
|
|
567
|
+
description: "Config, ownership, endpoint, and selected-provider health",
|
|
568
|
+
usage: "[provider] [--probe]",
|
|
569
|
+
},
|
|
570
|
+
{ name: "test", description: "Send a test notification", usage: "[provider|--provider provider] [message]" },
|
|
519
571
|
{ name: "recovery", description: "Clear dead-owner locks and stale endpoint files" },
|
|
520
572
|
{ name: "setup", description: "How to pair a Telegram bot (run in a terminal)" },
|
|
521
573
|
],
|
|
@@ -541,12 +593,41 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
541
593
|
await runtime.output(formatNotificationStatusReport(buildNotificationStatusReport(runtime.settings)));
|
|
542
594
|
return commandConsumed();
|
|
543
595
|
case "health": {
|
|
544
|
-
const
|
|
596
|
+
const parsed = parseNotifyServiceArgs(rest, false);
|
|
597
|
+
if ("error" in parsed) {
|
|
598
|
+
return usage(`Usage: /notify health [telegram|discord|slack] [--probe]\n${parsed.error}`, runtime);
|
|
599
|
+
}
|
|
600
|
+
const report = await checkNotificationHealth({
|
|
601
|
+
settings: runtime.settings,
|
|
602
|
+
stateRoot,
|
|
603
|
+
provider: parsed.provider,
|
|
604
|
+
probe: parsed.probe,
|
|
605
|
+
});
|
|
545
606
|
await runtime.output(formatNotificationHealthReport(report));
|
|
546
607
|
return commandConsumed();
|
|
547
608
|
}
|
|
548
609
|
case "test": {
|
|
549
|
-
const
|
|
610
|
+
const parsed = parseNotifyServiceArgs(rest, true);
|
|
611
|
+
if ("error" in parsed) {
|
|
612
|
+
return usage(
|
|
613
|
+
`Usage: /notify test [telegram|discord|slack|--provider provider] [message]\n${parsed.error}`,
|
|
614
|
+
runtime,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
const result = await sendNotificationTest({
|
|
618
|
+
settings: runtime.settings,
|
|
619
|
+
provider: parsed.provider,
|
|
620
|
+
text: parsed.message,
|
|
621
|
+
deps: {
|
|
622
|
+
providerRuntimeStatus: async provider => {
|
|
623
|
+
const status =
|
|
624
|
+
provider === "telegram"
|
|
625
|
+
? await new TelegramDaemonController(runtime.settings).status()
|
|
626
|
+
: await new ChatDaemonController(runtime.settings, provider).status();
|
|
627
|
+
return status.health === "running" ? "ready" : "inactive";
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
});
|
|
550
631
|
await runtime.output(formatNotificationTestResult(result));
|
|
551
632
|
return commandConsumed();
|
|
552
633
|
}
|
|
@@ -1286,20 +1367,45 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1286
1367
|
{
|
|
1287
1368
|
name: "login",
|
|
1288
1369
|
description: "Login with OAuth provider",
|
|
1289
|
-
inlineHint: "[provider|redirect URL]",
|
|
1370
|
+
inlineHint: "[provider|redirect URL] [--manual]",
|
|
1290
1371
|
allowArgs: true,
|
|
1291
1372
|
handleTui: (command, runtime) => {
|
|
1292
1373
|
const manualInput = runtime.ctx.oauthManualInput;
|
|
1293
1374
|
const args = command.args.trim();
|
|
1375
|
+
const pendingLoginMessage = (): string => {
|
|
1376
|
+
const pendingProvider = manualInput.pendingProviderId;
|
|
1377
|
+
return pendingProvider
|
|
1378
|
+
? `OAuth login already in progress for ${pendingProvider}. Paste the redirect URL with /login <url>.`
|
|
1379
|
+
: "OAuth login already in progress. Paste the redirect URL with /login <url>.";
|
|
1380
|
+
};
|
|
1294
1381
|
if (args.length > 0) {
|
|
1382
|
+
const tokens = args.split(/\s+/);
|
|
1383
|
+
// `--manual` is resolved before the paste fallback below, otherwise
|
|
1384
|
+
// `/login anthropic --manual` would be submitted as an authorization code.
|
|
1385
|
+
if (tokens.includes(MANUAL_LOGIN_FLAG)) {
|
|
1386
|
+
const rest = tokens.filter(token => token !== MANUAL_LOGIN_FLAG);
|
|
1387
|
+
const requestedProvider = rest.length === 1 ? rest[0] : undefined;
|
|
1388
|
+
const manualProvider = requestedProvider
|
|
1389
|
+
? getOAuthProviders().find(provider => provider.id === requestedProvider)
|
|
1390
|
+
: undefined;
|
|
1391
|
+
if (!manualProvider) {
|
|
1392
|
+
runtime.ctx.showWarning(`Usage: /login <provider> ${MANUAL_LOGIN_FLAG}`);
|
|
1393
|
+
runtime.ctx.editor.setText("");
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
if (manualInput.hasPending()) {
|
|
1397
|
+
runtime.ctx.showWarning(pendingLoginMessage());
|
|
1398
|
+
runtime.ctx.editor.setText("");
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
void runtime.ctx.showOAuthSelector("login", manualProvider.id, { manualCode: true });
|
|
1402
|
+
runtime.ctx.editor.setText("");
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1295
1405
|
const matchedProvider = getOAuthProviders().find(provider => provider.id === args);
|
|
1296
1406
|
if (matchedProvider) {
|
|
1297
1407
|
if (manualInput.hasPending()) {
|
|
1298
|
-
|
|
1299
|
-
const message = pendingProvider
|
|
1300
|
-
? `OAuth login already in progress for ${pendingProvider}. Paste the redirect URL with /login <url>.`
|
|
1301
|
-
: "OAuth login already in progress. Paste the redirect URL with /login <url>.";
|
|
1302
|
-
runtime.ctx.showWarning(message);
|
|
1408
|
+
runtime.ctx.showWarning(pendingLoginMessage());
|
|
1303
1409
|
runtime.ctx.editor.setText("");
|
|
1304
1410
|
return;
|
|
1305
1411
|
}
|
|
@@ -1318,11 +1424,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1318
1424
|
}
|
|
1319
1425
|
|
|
1320
1426
|
if (manualInput.hasPending()) {
|
|
1321
|
-
|
|
1322
|
-
const message = provider
|
|
1323
|
-
? `OAuth login already in progress for ${provider}. Paste the redirect URL with /login <url>.`
|
|
1324
|
-
: "OAuth login already in progress. Paste the redirect URL with /login <url>.";
|
|
1325
|
-
runtime.ctx.showWarning(message);
|
|
1427
|
+
runtime.ctx.showWarning(pendingLoginMessage());
|
|
1326
1428
|
runtime.ctx.editor.setText("");
|
|
1327
1429
|
return;
|
|
1328
1430
|
}
|