@gajae-code/coding-agent 0.15.6 → 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 +59 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -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-sidecar.d.ts +66 -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/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/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/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +77 -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/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/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/session-runtime.d.ts +52 -0
- 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/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/credential-store-errors.d.ts +1 -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/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +77 -11
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +1 -5
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +23 -0
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +8 -1
- package/src/config/settings.ts +5 -0
- package/src/coordinator-mcp/projection-scan.ts +21 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +94 -20
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- 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 +79 -6
- package/src/gjc-runtime/session-state-sidecar.ts +929 -23
- 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/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +57 -2
- 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 +605 -156
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- 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 +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/input-controller.ts +28 -0
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/prompts/system/master-mode.md +28 -0
- package/src/sdk/broker/broker.ts +1636 -26
- 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 +186 -41
- 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 +35 -4
- package/src/sdk/bus/index.ts +51 -4
- 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 +264 -5
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +99 -38
- 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/session-runtime.test.ts +210 -6
- package/src/sdk/host/session-runtime.ts +291 -49
- 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/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +13 -3
- package/src/sdk/router/session-router.ts +67 -59
- package/src/sdk/session.ts +357 -155
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/agent-session.ts +640 -103
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +337 -24
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/bash.ts +256 -42
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +4 -0
|
@@ -7299,8 +7299,49 @@ export class SessionManager {
|
|
|
7299
7299
|
/** Serializes model, SDK, and ACP cwd transitions; dispose joins this tail. */
|
|
7300
7300
|
#cwdTransitionTail: Promise<void> = Promise.resolve();
|
|
7301
7301
|
#cwdTransitionOwner: symbol | undefined;
|
|
7302
|
+
#cwdMoveAdmissionClosed = false;
|
|
7302
7303
|
#cwdReadLeaseOwner = Symbol("cwd-read-lease-owner");
|
|
7303
7304
|
#cwdGeneration = 0;
|
|
7305
|
+
/**
|
|
7306
|
+
* Listeners invoked after every committed `moveTo`, for every move surface
|
|
7307
|
+
* (`move_session`, TUI `/move`, SDK/ACP `session.cwd.move`). They receive the previous
|
|
7308
|
+
* and new cwd and the previous session file, and run best-effort: a listener throw is
|
|
7309
|
+
* logged and never turns an already-durable move into a rejection.
|
|
7310
|
+
*/
|
|
7311
|
+
#afterMoveListeners = new Set<
|
|
7312
|
+
(move: { previousCwd: string; newCwd: string; previousSessionFile: string | undefined }) => void | Promise<void>
|
|
7313
|
+
>();
|
|
7314
|
+
#beforeMoveListeners = new Set<
|
|
7315
|
+
(move: {
|
|
7316
|
+
previousCwd: string;
|
|
7317
|
+
newCwd: string;
|
|
7318
|
+
previousSessionFile: string | undefined;
|
|
7319
|
+
newSessionFile: string | undefined;
|
|
7320
|
+
previousCwdIdentity: { dev: bigint; ino: bigint };
|
|
7321
|
+
newCwdIdentity: { dev: bigint; ino: bigint };
|
|
7322
|
+
}) => void | Promise<void>
|
|
7323
|
+
>();
|
|
7324
|
+
#moveAbortListeners = new Set<
|
|
7325
|
+
(move: {
|
|
7326
|
+
previousCwd: string;
|
|
7327
|
+
newCwd: string;
|
|
7328
|
+
previousSessionFile: string | undefined;
|
|
7329
|
+
newSessionFile: string | undefined;
|
|
7330
|
+
previousCwdIdentity: { dev: bigint; ino: bigint };
|
|
7331
|
+
newCwdIdentity: { dev: bigint; ino: bigint };
|
|
7332
|
+
preserveRecoveryJournal?: boolean;
|
|
7333
|
+
}) => void | Promise<void>
|
|
7334
|
+
>();
|
|
7335
|
+
#movePublicationListeners = new Set<
|
|
7336
|
+
(move: {
|
|
7337
|
+
previousCwd: string;
|
|
7338
|
+
newCwd: string;
|
|
7339
|
+
previousSessionFile: string | undefined;
|
|
7340
|
+
newSessionFile: string | undefined;
|
|
7341
|
+
previousCwdIdentity: { dev: bigint; ino: bigint };
|
|
7342
|
+
newCwdIdentity: { dev: bigint; ino: bigint };
|
|
7343
|
+
}) => void | Promise<void>
|
|
7344
|
+
>();
|
|
7304
7345
|
/** Number of tool executions currently holding a shared read lease on `cwd`. */
|
|
7305
7346
|
#cwdReaderCount = 0;
|
|
7306
7347
|
/** Resolved when the last outstanding read lease is released. */
|
|
@@ -10728,10 +10769,139 @@ export class SessionManager {
|
|
|
10728
10769
|
async joinCwdTransition(): Promise<void> {
|
|
10729
10770
|
await this.#cwdTransitionTail;
|
|
10730
10771
|
}
|
|
10772
|
+
|
|
10773
|
+
async closeCwdMoveAdmission(): Promise<void> {
|
|
10774
|
+
await this.runExclusiveCwdTransition(async () => {
|
|
10775
|
+
this.#cwdMoveAdmissionClosed = true;
|
|
10776
|
+
});
|
|
10777
|
+
}
|
|
10731
10778
|
getCwdGeneration(): number {
|
|
10732
10779
|
return this.#cwdGeneration;
|
|
10733
10780
|
}
|
|
10734
10781
|
|
|
10782
|
+
/**
|
|
10783
|
+
* Register a listener invoked after every committed `moveTo`. Returns an idempotent
|
|
10784
|
+
* unregister handle. Every move surface funnels through `moveTo`, so a single
|
|
10785
|
+
* registration covers `move_session`, `/move`, and SDK/ACP `session.cwd.move`.
|
|
10786
|
+
*/
|
|
10787
|
+
registerAfterMoveListener(
|
|
10788
|
+
listener: (move: {
|
|
10789
|
+
previousCwd: string;
|
|
10790
|
+
newCwd: string;
|
|
10791
|
+
previousSessionFile: string | undefined;
|
|
10792
|
+
}) => void | Promise<void>,
|
|
10793
|
+
): () => void {
|
|
10794
|
+
this.#afterMoveListeners.add(listener);
|
|
10795
|
+
return () => {
|
|
10796
|
+
this.#afterMoveListeners.delete(listener);
|
|
10797
|
+
};
|
|
10798
|
+
}
|
|
10799
|
+
|
|
10800
|
+
registerBeforeMoveListener(
|
|
10801
|
+
listener: (move: {
|
|
10802
|
+
previousCwd: string;
|
|
10803
|
+
newCwd: string;
|
|
10804
|
+
previousSessionFile: string | undefined;
|
|
10805
|
+
newSessionFile: string | undefined;
|
|
10806
|
+
previousCwdIdentity: { dev: bigint; ino: bigint };
|
|
10807
|
+
newCwdIdentity: { dev: bigint; ino: bigint };
|
|
10808
|
+
preserveRecoveryJournal?: boolean;
|
|
10809
|
+
}) => void | Promise<void>,
|
|
10810
|
+
): () => void {
|
|
10811
|
+
this.#beforeMoveListeners.add(listener);
|
|
10812
|
+
return () => {
|
|
10813
|
+
this.#beforeMoveListeners.delete(listener);
|
|
10814
|
+
};
|
|
10815
|
+
}
|
|
10816
|
+
|
|
10817
|
+
async #runBeforeMoveListeners(move: {
|
|
10818
|
+
previousCwd: string;
|
|
10819
|
+
newCwd: string;
|
|
10820
|
+
previousSessionFile: string | undefined;
|
|
10821
|
+
newSessionFile: string | undefined;
|
|
10822
|
+
previousCwdIdentity: { dev: bigint; ino: bigint };
|
|
10823
|
+
newCwdIdentity: { dev: bigint; ino: bigint };
|
|
10824
|
+
}): Promise<void> {
|
|
10825
|
+
for (const listener of [...this.#beforeMoveListeners]) await listener(move);
|
|
10826
|
+
}
|
|
10827
|
+
|
|
10828
|
+
registerMovePublicationListener(
|
|
10829
|
+
listener: (move: {
|
|
10830
|
+
previousCwd: string;
|
|
10831
|
+
newCwd: string;
|
|
10832
|
+
previousSessionFile: string | undefined;
|
|
10833
|
+
newSessionFile: string | undefined;
|
|
10834
|
+
previousCwdIdentity: { dev: bigint; ino: bigint };
|
|
10835
|
+
newCwdIdentity: { dev: bigint; ino: bigint };
|
|
10836
|
+
preserveRecoveryJournal?: boolean;
|
|
10837
|
+
}) => void | Promise<void>,
|
|
10838
|
+
): () => void {
|
|
10839
|
+
this.#movePublicationListeners.add(listener);
|
|
10840
|
+
return () => {
|
|
10841
|
+
this.#movePublicationListeners.delete(listener);
|
|
10842
|
+
};
|
|
10843
|
+
}
|
|
10844
|
+
|
|
10845
|
+
async #runMovePublicationListeners(move: {
|
|
10846
|
+
previousCwd: string;
|
|
10847
|
+
newCwd: string;
|
|
10848
|
+
previousSessionFile: string | undefined;
|
|
10849
|
+
newSessionFile: string | undefined;
|
|
10850
|
+
previousCwdIdentity: { dev: bigint; ino: bigint };
|
|
10851
|
+
newCwdIdentity: { dev: bigint; ino: bigint };
|
|
10852
|
+
}): Promise<void> {
|
|
10853
|
+
for (const listener of [...this.#movePublicationListeners]) await listener(move);
|
|
10854
|
+
}
|
|
10855
|
+
|
|
10856
|
+
registerMoveAbortListener(
|
|
10857
|
+
listener: (move: {
|
|
10858
|
+
previousCwd: string;
|
|
10859
|
+
newCwd: string;
|
|
10860
|
+
previousSessionFile: string | undefined;
|
|
10861
|
+
newSessionFile: string | undefined;
|
|
10862
|
+
previousCwdIdentity: { dev: bigint; ino: bigint };
|
|
10863
|
+
newCwdIdentity: { dev: bigint; ino: bigint };
|
|
10864
|
+
preserveRecoveryJournal?: boolean;
|
|
10865
|
+
}) => void | Promise<void>,
|
|
10866
|
+
): () => void {
|
|
10867
|
+
this.#moveAbortListeners.add(listener);
|
|
10868
|
+
return () => {
|
|
10869
|
+
this.#moveAbortListeners.delete(listener);
|
|
10870
|
+
};
|
|
10871
|
+
}
|
|
10872
|
+
|
|
10873
|
+
async #runMoveAbortListeners(move: {
|
|
10874
|
+
previousCwd: string;
|
|
10875
|
+
newCwd: string;
|
|
10876
|
+
previousSessionFile: string | undefined;
|
|
10877
|
+
newSessionFile: string | undefined;
|
|
10878
|
+
previousCwdIdentity: { dev: bigint; ino: bigint };
|
|
10879
|
+
newCwdIdentity: { dev: bigint; ino: bigint };
|
|
10880
|
+
preserveRecoveryJournal?: boolean;
|
|
10881
|
+
}): Promise<void> {
|
|
10882
|
+
for (const listener of [...this.#moveAbortListeners]) {
|
|
10883
|
+
try {
|
|
10884
|
+
await listener(move);
|
|
10885
|
+
} catch (error) {
|
|
10886
|
+
logger.warn("move-abort listener failed", { error: String(error), cwd: move.newCwd });
|
|
10887
|
+
}
|
|
10888
|
+
}
|
|
10889
|
+
}
|
|
10890
|
+
|
|
10891
|
+
async #runAfterMoveListeners(move: {
|
|
10892
|
+
previousCwd: string;
|
|
10893
|
+
newCwd: string;
|
|
10894
|
+
previousSessionFile: string | undefined;
|
|
10895
|
+
}): Promise<void> {
|
|
10896
|
+
for (const listener of [...this.#afterMoveListeners]) {
|
|
10897
|
+
try {
|
|
10898
|
+
await listener(move);
|
|
10899
|
+
} catch (error) {
|
|
10900
|
+
logger.warn("after-move listener failed", { error: String(error), cwd: move.newCwd });
|
|
10901
|
+
}
|
|
10902
|
+
}
|
|
10903
|
+
}
|
|
10904
|
+
|
|
10735
10905
|
#ownsCwdTransition(): boolean {
|
|
10736
10906
|
const owner = this.#cwdTransitionOwner;
|
|
10737
10907
|
return owner !== undefined && cwdTransitionAls.getStore() === owner;
|
|
@@ -10802,15 +10972,73 @@ export class SessionManager {
|
|
|
10802
10972
|
options?: {
|
|
10803
10973
|
expectedIdentity?: { dev: bigint; ino: bigint };
|
|
10804
10974
|
targetHandle?: { stat: (opts: { bigint: true }) => Promise<fs.BigIntStats> };
|
|
10975
|
+
expectedSourceIdentity?: { dev: bigint; ino: bigint };
|
|
10976
|
+
sourceHandle?: { stat: (opts: { bigint: true }) => Promise<fs.BigIntStats> };
|
|
10805
10977
|
},
|
|
10806
10978
|
): Promise<void> {
|
|
10807
10979
|
if (!this.#ownsCwdTransition()) {
|
|
10808
10980
|
return this.runExclusiveCwdTransition(() => this.moveTo(newCwd, options));
|
|
10809
10981
|
}
|
|
10810
|
-
|
|
10982
|
+
if (this.#cwdMoveAdmissionClosed) throw new Error("Session cwd move admission is closed.");
|
|
10983
|
+
if (!options?.sourceHandle) {
|
|
10984
|
+
const sourceHandle = await SessionManager.openNoFollowDirectory(this.cwd);
|
|
10985
|
+
try {
|
|
10986
|
+
const sourceIdentity = await sourceHandle.stat({ bigint: true });
|
|
10987
|
+
if (!sourceIdentity.isDirectory()) throw new Error(`Current cwd is not a directory: ${this.cwd}`);
|
|
10988
|
+
return await this.moveTo(newCwd, {
|
|
10989
|
+
...options,
|
|
10990
|
+
expectedSourceIdentity: options?.expectedSourceIdentity ?? {
|
|
10991
|
+
dev: sourceIdentity.dev,
|
|
10992
|
+
ino: sourceIdentity.ino,
|
|
10993
|
+
},
|
|
10994
|
+
sourceHandle,
|
|
10995
|
+
});
|
|
10996
|
+
} finally {
|
|
10997
|
+
await sourceHandle.close();
|
|
10998
|
+
}
|
|
10999
|
+
}
|
|
11000
|
+
const resolvedCwd = await fs.promises.realpath(path.resolve(newCwd));
|
|
11001
|
+
const resolvedIdentity = await fs.promises.lstat(resolvedCwd, { bigint: true });
|
|
11002
|
+
if (!resolvedIdentity.isDirectory() || resolvedIdentity.isSymbolicLink()) {
|
|
11003
|
+
throw new Error(`Refusing to rescope: target is not a directory: ${resolvedCwd}`);
|
|
11004
|
+
}
|
|
11005
|
+
if (!options?.targetHandle) {
|
|
11006
|
+
const targetHandle = await SessionManager.openNoFollowDirectory(resolvedCwd);
|
|
11007
|
+
try {
|
|
11008
|
+
const targetIdentity = await targetHandle.stat({ bigint: true });
|
|
11009
|
+
const revalidatedCwd = await fs.promises.realpath(path.resolve(newCwd));
|
|
11010
|
+
const namedIdentity = await fs.promises.lstat(revalidatedCwd, { bigint: true });
|
|
11011
|
+
if (
|
|
11012
|
+
revalidatedCwd !== resolvedCwd ||
|
|
11013
|
+
!namedIdentity.isDirectory() ||
|
|
11014
|
+
namedIdentity.isSymbolicLink() ||
|
|
11015
|
+
namedIdentity.dev !== resolvedIdentity.dev ||
|
|
11016
|
+
namedIdentity.ino !== resolvedIdentity.ino ||
|
|
11017
|
+
targetIdentity.dev !== resolvedIdentity.dev ||
|
|
11018
|
+
targetIdentity.ino !== resolvedIdentity.ino
|
|
11019
|
+
)
|
|
11020
|
+
throw new Error(`Refusing to rescope: target identity changed: ${resolvedCwd}`);
|
|
11021
|
+
return await this.moveTo(resolvedCwd, {
|
|
11022
|
+
...options,
|
|
11023
|
+
expectedIdentity: options?.expectedIdentity ?? {
|
|
11024
|
+
dev: targetIdentity.dev,
|
|
11025
|
+
ino: targetIdentity.ino,
|
|
11026
|
+
},
|
|
11027
|
+
targetHandle,
|
|
11028
|
+
});
|
|
11029
|
+
} finally {
|
|
11030
|
+
await targetHandle.close();
|
|
11031
|
+
}
|
|
11032
|
+
}
|
|
10811
11033
|
if (options?.expectedIdentity || options?.targetHandle) {
|
|
10812
11034
|
await this.#assertCwdTargetIdentity(resolvedCwd, options);
|
|
10813
11035
|
}
|
|
11036
|
+
if (options?.expectedSourceIdentity || options?.sourceHandle) {
|
|
11037
|
+
await this.#assertCwdTargetIdentity(this.cwd, {
|
|
11038
|
+
expectedIdentity: options.expectedSourceIdentity,
|
|
11039
|
+
targetHandle: options.sourceHandle,
|
|
11040
|
+
});
|
|
11041
|
+
}
|
|
10814
11042
|
if (resolvedCwd === this.cwd) return;
|
|
10815
11043
|
const previousCwd = this.cwd;
|
|
10816
11044
|
const previousSessionDir = this.sessionDir;
|
|
@@ -10826,6 +11054,19 @@ export class SessionManager {
|
|
|
10826
11054
|
)
|
|
10827
11055
|
: this.destination;
|
|
10828
11056
|
const newSessionDir = nextDestination.directory;
|
|
11057
|
+
const nextSessionFile = this.#sessionFile
|
|
11058
|
+
? path.join(newSessionDir, path.basename(this.#sessionFile))
|
|
11059
|
+
: undefined;
|
|
11060
|
+
const pinnedSourceIdentity = await options.sourceHandle.stat({ bigint: true });
|
|
11061
|
+
const pinnedTargetIdentity = await options.targetHandle.stat({ bigint: true });
|
|
11062
|
+
const moveDetails = {
|
|
11063
|
+
previousCwd,
|
|
11064
|
+
newCwd: resolvedCwd,
|
|
11065
|
+
previousSessionFile,
|
|
11066
|
+
newSessionFile: nextSessionFile,
|
|
11067
|
+
previousCwdIdentity: { dev: pinnedSourceIdentity.dev, ino: pinnedSourceIdentity.ino },
|
|
11068
|
+
newCwdIdentity: { dev: pinnedTargetIdentity.dev, ino: pinnedTargetIdentity.ino },
|
|
11069
|
+
};
|
|
10829
11070
|
let hadSessionFile = false;
|
|
10830
11071
|
const managedMove = this.destination.kind === "managed" && nextDestination.kind === "managed";
|
|
10831
11072
|
let managedSourceStore: ManagedSessionDescendantStore | undefined;
|
|
@@ -10833,16 +11074,29 @@ export class SessionManager {
|
|
|
10833
11074
|
let rollbackManagedMove: (() => Promise<void>) | undefined;
|
|
10834
11075
|
let residentTransition: PreparedResidentStoreTransition | undefined;
|
|
10835
11076
|
const hadPersistedSession = this.#ensuredOnDisk;
|
|
11077
|
+
try {
|
|
11078
|
+
await this.#runBeforeMoveListeners(moveDetails);
|
|
11079
|
+
} catch (error) {
|
|
11080
|
+
await this.#runMoveAbortListeners(moveDetails);
|
|
11081
|
+
throw error;
|
|
11082
|
+
}
|
|
10836
11083
|
|
|
11084
|
+
let targetIdentityRevalidated = false;
|
|
11085
|
+
let publicationStarted = false;
|
|
10837
11086
|
if (this.persist && this.#sessionFile) {
|
|
10838
11087
|
// Close the persist writer before moving files
|
|
10839
|
-
|
|
11088
|
+
try {
|
|
11089
|
+
await this.#closePersistWriter();
|
|
11090
|
+
} catch (error) {
|
|
11091
|
+
await this.#runMoveAbortListeners(moveDetails);
|
|
11092
|
+
throw error;
|
|
11093
|
+
}
|
|
10840
11094
|
this.#persistChain = Promise.resolve();
|
|
10841
11095
|
this.#persistError = undefined;
|
|
10842
11096
|
this.#persistErrorReported = false;
|
|
10843
11097
|
|
|
10844
11098
|
const oldSessionFile = this.#sessionFile;
|
|
10845
|
-
const newSessionFile =
|
|
11099
|
+
const newSessionFile = nextSessionFile!;
|
|
10846
11100
|
const oldArtifactDir = oldSessionFile.slice(0, -6); // strip .jsonl
|
|
10847
11101
|
const newArtifactDir = newSessionFile.slice(0, -6);
|
|
10848
11102
|
let managedTranscript!: ReturnType<ManagedSessionDescendantStore["readExpected"]>;
|
|
@@ -10852,24 +11106,30 @@ export class SessionManager {
|
|
|
10852
11106
|
hadSessionFile = managedMove ? false : this.#storage.existsSync(oldSessionFile);
|
|
10853
11107
|
let movedSessionFile = false;
|
|
10854
11108
|
let movedArtifactDir = false;
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
11109
|
+
try {
|
|
11110
|
+
const materializedEntries = materializeResidentEntriesForReadSync(
|
|
11111
|
+
this.#fileEntries,
|
|
11112
|
+
this.#residentBlobStores(),
|
|
11113
|
+
);
|
|
11114
|
+
const transitionEntries: FileEntry[] = materializedEntries.map(entry =>
|
|
11115
|
+
entry.type === "session" ? { ...entry, cwd: resolvedCwd } : entry,
|
|
11116
|
+
);
|
|
11117
|
+
residentTransition = this.#prepareResidentTextStoreTransition(
|
|
11118
|
+
{
|
|
11119
|
+
target: { sessionId: this.#sessionId, sessionFile: newSessionFile },
|
|
11120
|
+
primary: {
|
|
11121
|
+
mode: "materialize",
|
|
11122
|
+
sourceEntries: transitionEntries,
|
|
11123
|
+
sourceStores: { textStore: null, imageStore: this.#residentImageBlobStore },
|
|
11124
|
+
},
|
|
10869
11125
|
},
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
)
|
|
11126
|
+
"retain-and-throw",
|
|
11127
|
+
);
|
|
11128
|
+
} catch (error) {
|
|
11129
|
+
residentTransition?.dispose();
|
|
11130
|
+
await this.#runMoveAbortListeners(moveDetails);
|
|
11131
|
+
throw error;
|
|
11132
|
+
}
|
|
10873
11133
|
const discardResidentTransitionAndThrow = (error: unknown): never => {
|
|
10874
11134
|
residentTransition?.dispose();
|
|
10875
11135
|
throw error;
|
|
@@ -10912,6 +11172,14 @@ export class SessionManager {
|
|
|
10912
11172
|
throw new Error("artifact_destination_mismatch");
|
|
10913
11173
|
destination.fsyncTree();
|
|
10914
11174
|
};
|
|
11175
|
+
try {
|
|
11176
|
+
await this.#runMovePublicationListeners(moveDetails);
|
|
11177
|
+
publicationStarted = true;
|
|
11178
|
+
} catch (error) {
|
|
11179
|
+
residentTransition?.dispose();
|
|
11180
|
+
await this.#runMoveAbortListeners(moveDetails);
|
|
11181
|
+
throw error;
|
|
11182
|
+
}
|
|
10915
11183
|
try {
|
|
10916
11184
|
if (managedMove) {
|
|
10917
11185
|
if (previousDestination.kind !== "managed" || nextDestination.kind !== "managed")
|
|
@@ -10997,6 +11265,16 @@ export class SessionManager {
|
|
|
10997
11265
|
if (!isEnoent(err)) throw err;
|
|
10998
11266
|
}
|
|
10999
11267
|
}
|
|
11268
|
+
if (options?.expectedIdentity || options?.targetHandle) {
|
|
11269
|
+
if (options.expectedSourceIdentity || options.sourceHandle) {
|
|
11270
|
+
await this.#assertCwdTargetIdentity(previousCwd, {
|
|
11271
|
+
expectedIdentity: options.expectedSourceIdentity,
|
|
11272
|
+
targetHandle: options.sourceHandle,
|
|
11273
|
+
});
|
|
11274
|
+
}
|
|
11275
|
+
await this.#assertCwdTargetIdentity(resolvedCwd, options);
|
|
11276
|
+
targetIdentityRevalidated = true;
|
|
11277
|
+
}
|
|
11000
11278
|
} catch (err) {
|
|
11001
11279
|
if (managedMove && managedDestinationStore) {
|
|
11002
11280
|
try {
|
|
@@ -11038,6 +11316,7 @@ export class SessionManager {
|
|
|
11038
11316
|
)
|
|
11039
11317
|
await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
|
|
11040
11318
|
} catch (rollbackErr) {
|
|
11319
|
+
await this.#runMoveAbortListeners({ ...moveDetails, preserveRecoveryJournal: true });
|
|
11041
11320
|
discardResidentTransitionAndThrow(
|
|
11042
11321
|
new Error(`Failed to rollback managed move: ${toError(rollbackErr).message}`, {
|
|
11043
11322
|
cause: toError(err),
|
|
@@ -11045,18 +11324,46 @@ export class SessionManager {
|
|
|
11045
11324
|
);
|
|
11046
11325
|
}
|
|
11047
11326
|
} else {
|
|
11048
|
-
|
|
11049
|
-
|
|
11327
|
+
try {
|
|
11328
|
+
if (movedArtifactDir) await movePathAcrossDevicesSafe(newArtifactDir, oldArtifactDir);
|
|
11329
|
+
if (movedSessionFile) await movePathAcrossDevicesSafe(newSessionFile, oldSessionFile);
|
|
11330
|
+
} catch (rollbackError) {
|
|
11331
|
+
await this.#runMoveAbortListeners({ ...moveDetails, preserveRecoveryJournal: true });
|
|
11332
|
+
discardResidentTransitionAndThrow(
|
|
11333
|
+
new Error(`Failed to rollback move: ${toError(rollbackError).message}`, { cause: toError(err) }),
|
|
11334
|
+
);
|
|
11335
|
+
}
|
|
11050
11336
|
}
|
|
11337
|
+
await this.#runMoveAbortListeners(moveDetails);
|
|
11051
11338
|
discardResidentTransitionAndThrow(err);
|
|
11052
11339
|
}
|
|
11053
11340
|
this.#sessionFile = newSessionFile;
|
|
11054
11341
|
}
|
|
11342
|
+
if (!publicationStarted) {
|
|
11343
|
+
try {
|
|
11344
|
+
await this.#runMovePublicationListeners(moveDetails);
|
|
11345
|
+
publicationStarted = true;
|
|
11346
|
+
} catch (error) {
|
|
11347
|
+
await this.#runMoveAbortListeners(moveDetails);
|
|
11348
|
+
throw error;
|
|
11349
|
+
}
|
|
11350
|
+
}
|
|
11055
11351
|
|
|
11056
11352
|
// Update cwd and sessionDir after physical publication succeeds. Metadata failures restore the source
|
|
11057
11353
|
// authority but deliberately retain any destination publication evidence rather than deleting it.
|
|
11058
|
-
if (options?.expectedIdentity || options?.targetHandle) {
|
|
11059
|
-
|
|
11354
|
+
if (!targetIdentityRevalidated && (options?.expectedIdentity || options?.targetHandle)) {
|
|
11355
|
+
try {
|
|
11356
|
+
if (options.expectedSourceIdentity || options.sourceHandle) {
|
|
11357
|
+
await this.#assertCwdTargetIdentity(previousCwd, {
|
|
11358
|
+
expectedIdentity: options.expectedSourceIdentity,
|
|
11359
|
+
targetHandle: options.sourceHandle,
|
|
11360
|
+
});
|
|
11361
|
+
}
|
|
11362
|
+
await this.#assertCwdTargetIdentity(resolvedCwd, options);
|
|
11363
|
+
} catch (error) {
|
|
11364
|
+
await this.#runMoveAbortListeners(moveDetails);
|
|
11365
|
+
throw error;
|
|
11366
|
+
}
|
|
11060
11367
|
}
|
|
11061
11368
|
this.#cwdGeneration += 1;
|
|
11062
11369
|
this.cwd = resolvedCwd;
|
|
@@ -11096,6 +11403,7 @@ export class SessionManager {
|
|
|
11096
11403
|
await rollbackManagedMove();
|
|
11097
11404
|
} catch (rollbackError) {
|
|
11098
11405
|
if (toError(rollbackError).message !== "cleanup_pending") {
|
|
11406
|
+
await this.#runMoveAbortListeners({ ...moveDetails, preserveRecoveryJournal: true });
|
|
11099
11407
|
throw new Error(`Failed to rollback managed move: ${toError(rollbackError).message}`, {
|
|
11100
11408
|
cause: toError(error),
|
|
11101
11409
|
});
|
|
@@ -11116,12 +11424,14 @@ export class SessionManager {
|
|
|
11116
11424
|
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
11117
11425
|
if (header) applyHeaderPatch(header, { cwd: previousCwd });
|
|
11118
11426
|
this.#headerExportRevision++;
|
|
11427
|
+
await this.#runMoveAbortListeners(moveDetails);
|
|
11119
11428
|
throw error;
|
|
11120
11429
|
}
|
|
11121
11430
|
// The destination has already been published. Retain it rather than moving it
|
|
11122
11431
|
// back over the source after a metadata failure.
|
|
11123
11432
|
this.#managedTranscriptStoreCache = null;
|
|
11124
11433
|
this.#headerExportRevision++;
|
|
11434
|
+
await this.#runAfterMoveListeners(moveDetails);
|
|
11125
11435
|
throw error;
|
|
11126
11436
|
}
|
|
11127
11437
|
if (residentTransition) this.#commitResidentTextStoreTransition(residentTransition);
|
|
@@ -11130,6 +11440,9 @@ export class SessionManager {
|
|
|
11130
11440
|
if (this.#sessionFile) {
|
|
11131
11441
|
writeTerminalBreadcrumb(resolvedCwd, this.#sessionFile);
|
|
11132
11442
|
}
|
|
11443
|
+
// The move is fully committed here; notify every surface's after-move listeners
|
|
11444
|
+
// (coordinator runtime-state relocation, postmortem finalizer rebinding, ...).
|
|
11445
|
+
await this.#runAfterMoveListeners(moveDetails);
|
|
11133
11446
|
}
|
|
11134
11447
|
|
|
11135
11448
|
/** Sync version for initial creation (no existing writer to close). */
|
|
@@ -18,8 +18,9 @@ export interface YieldDispatcher<P> {
|
|
|
18
18
|
export interface YieldQueueOptions {
|
|
19
19
|
isStreaming: () => boolean;
|
|
20
20
|
injectStreaming(msg: AgentMessage): void;
|
|
21
|
-
injectIdle(messages: AgentMessage[]): Promise<void>;
|
|
22
|
-
scheduleIdleFlush(run: () => Promise<void
|
|
21
|
+
injectIdle(messages: AgentMessage[], signal?: AbortSignal): Promise<void>;
|
|
22
|
+
scheduleIdleFlush(run: (signal?: AbortSignal) => Promise<void>, onSkip: () => void): void;
|
|
23
|
+
getIdleFlushSignal?(): AbortSignal | undefined;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
type YieldFlushMode = "streaming" | "idle";
|
|
@@ -39,6 +40,7 @@ export class YieldQueue {
|
|
|
39
40
|
readonly #dispatchers = new Map<string, StoredDispatcher>();
|
|
40
41
|
readonly #entries = new Map<string, unknown[]>();
|
|
41
42
|
#idleFlushPending = false;
|
|
43
|
+
#idleFlushPendingOwner: symbol | undefined;
|
|
42
44
|
|
|
43
45
|
constructor(options: YieldQueueOptions) {
|
|
44
46
|
this.#options = options;
|
|
@@ -82,9 +84,10 @@ export class YieldQueue {
|
|
|
82
84
|
return false;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
async flush(mode: YieldFlushMode): Promise<void> {
|
|
87
|
+
async flush(mode: YieldFlushMode, signal?: AbortSignal): Promise<void> {
|
|
86
88
|
if (mode === "idle") {
|
|
87
89
|
this.#idleFlushPending = false;
|
|
90
|
+
this.#idleFlushPendingOwner = undefined;
|
|
88
91
|
}
|
|
89
92
|
const idleMessages: AgentMessage[] = [];
|
|
90
93
|
for (const [kind, dispatcher] of this.#dispatchers) {
|
|
@@ -105,16 +108,20 @@ export class YieldQueue {
|
|
|
105
108
|
}
|
|
106
109
|
if (mode === "idle" && idleMessages.length > 0) {
|
|
107
110
|
try {
|
|
108
|
-
await this.#options.injectIdle(idleMessages);
|
|
111
|
+
await this.#options.injectIdle(idleMessages, signal ?? this.#options.getIdleFlushSignal?.());
|
|
109
112
|
} catch (error) {
|
|
110
113
|
logger.warn("Yield queue idle dispatch failed", { error: formatError(error) });
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
clear(): void {
|
|
118
|
+
clear(onDrop?: (kind: string, entries: readonly unknown[]) => void): void {
|
|
119
|
+
if (onDrop) {
|
|
120
|
+
for (const [kind, entries] of this.#entries) onDrop(kind, entries);
|
|
121
|
+
}
|
|
116
122
|
this.#entries.clear();
|
|
117
123
|
this.#idleFlushPending = false;
|
|
124
|
+
this.#idleFlushPendingOwner = undefined;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
/** Drop only the queued entries of a single kind, leaving other kinds intact. */
|
|
@@ -140,14 +147,21 @@ export class YieldQueue {
|
|
|
140
147
|
#scheduleIdleFlush(): void {
|
|
141
148
|
if (this.#idleFlushPending) return;
|
|
142
149
|
this.#idleFlushPending = true;
|
|
150
|
+
const owner = Symbol("idle-flush");
|
|
151
|
+
this.#idleFlushPendingOwner = owner;
|
|
152
|
+
const releaseOwner = () => {
|
|
153
|
+
if (this.#idleFlushPendingOwner !== owner) return;
|
|
154
|
+
this.#idleFlushPendingOwner = undefined;
|
|
155
|
+
this.#idleFlushPending = false;
|
|
156
|
+
};
|
|
143
157
|
try {
|
|
144
|
-
this.#options.scheduleIdleFlush(async
|
|
145
|
-
|
|
158
|
+
this.#options.scheduleIdleFlush(async signal => {
|
|
159
|
+
releaseOwner();
|
|
146
160
|
if (this.#options.isStreaming()) return;
|
|
147
|
-
await this.flush("idle");
|
|
148
|
-
});
|
|
161
|
+
await this.flush("idle", signal);
|
|
162
|
+
}, releaseOwner);
|
|
149
163
|
} catch (error) {
|
|
150
|
-
|
|
164
|
+
releaseOwner();
|
|
151
165
|
logger.warn("Yield queue idle flush scheduling failed", { error: formatError(error) });
|
|
152
166
|
}
|
|
153
167
|
}
|
|
@@ -748,6 +748,7 @@ export function shouldOfferOnboarding(state: FrictionlessOnboardingState): boole
|
|
|
748
748
|
|
|
749
749
|
export interface AutomaticOnboardingInput {
|
|
750
750
|
normalInteractive: boolean;
|
|
751
|
+
automation?: boolean;
|
|
751
752
|
initialMessage?: string;
|
|
752
753
|
initialMessages: readonly string[];
|
|
753
754
|
initialImages?: readonly unknown[];
|
|
@@ -757,6 +758,7 @@ export interface AutomaticOnboardingInput {
|
|
|
757
758
|
export function shouldOfferAutomaticOnboarding(input: AutomaticOnboardingInput): boolean {
|
|
758
759
|
return (
|
|
759
760
|
input.normalInteractive &&
|
|
761
|
+
!input.automation &&
|
|
760
762
|
!input.initialMessage &&
|
|
761
763
|
input.initialMessages.length === 0 &&
|
|
762
764
|
(input.initialImages?.length ?? 0) === 0 &&
|