@keyou007/agent-bot 0.1.24-alpha.5 → 0.1.24-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +27 -3
- package/README.zh.md +27 -3
- package/SECURITY.md +12 -0
- package/config.example.yaml +5 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/serverStatus.js +1 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +202 -115
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +12 -13
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/config/AgentExecutionDefaultsStore.js +4 -0
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +3 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +66 -8
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +1 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +121 -17
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +43 -6
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +51 -4
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +7 -2
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +41 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +615 -214
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/proxy/SessionOperationQueue.js +14 -0
- package/dist/proxy/SessionOperationQueue.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +4 -2
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/state/StateStore.js +22 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +19 -7
- package/dist/state/TurnPreviewJournal.js.map +1 -1
- package/dist/state/migrations.js +6 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +1 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +19 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/terminal/TerminalCards.js +13 -0
- package/dist/terminal/TerminalCards.js.map +1 -0
- package/dist/terminal/TerminalLinks.js +76 -0
- package/dist/terminal/TerminalLinks.js.map +1 -0
- package/dist/terminal/TerminalPage.js +212 -0
- package/dist/terminal/TerminalPage.js.map +1 -0
- package/dist/terminal/TerminalProcess.js +71 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +639 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +48 -0
- package/docs/technical-reference.zh.md +48 -0
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +9 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/CodexRuntime.ts +200 -123
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/config/AgentExecutionDefaultsStore.ts +2 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +5 -1
- package/src/feishu/CardRenderer.ts +81 -8
- package/src/feishu/FeishuConnector.ts +1 -0
- package/src/feishu/FeishuTurnPresenter.ts +108 -16
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +44 -7
- package/src/local-files/LocalFileViewerServer.ts +48 -4
- package/src/local-files/TurnPreviewPage.ts +7 -2
- package/src/presentation/TurnStateReducer.ts +46 -7
- package/src/presentation/turnViewTypes.ts +5 -0
- package/src/proxy/ProxySessionController.ts +625 -211
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +8 -3
- package/src/state/StateStore.ts +38 -1
- package/src/state/TurnPreviewJournal.ts +19 -7
- package/src/state/migrations.ts +6 -0
- package/src/supervision/SafeRestartNotifier.ts +1 -0
- package/src/supervision/SafeRestartScheduler.ts +18 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +71 -0
- package/src/terminal/TerminalServer.ts +513 -0
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
AgentRuntime,
|
|
5
5
|
AgentProcessInfo,
|
|
6
6
|
ApprovalDecision,
|
|
7
|
+
ApprovalRequest,
|
|
7
8
|
CreateRuntimeSessionInput,
|
|
8
9
|
ConversationTurn,
|
|
9
10
|
ForkRuntimeSessionInput,
|
|
@@ -35,7 +36,7 @@ import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
|
|
|
35
36
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
36
37
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
37
38
|
import { threadWriterLockPath } from "./ThreadWriterProcess.js";
|
|
38
|
-
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
39
|
+
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, PROVIDER_SWITCH_EMPTY, ProviderSwitchError, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
39
40
|
import { fetchProviderModels, type ProviderModelConfig } from "./ProviderModels.js";
|
|
40
41
|
|
|
41
42
|
const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
@@ -84,6 +85,7 @@ interface CodexSession extends RuntimeSession {
|
|
|
84
85
|
activeTurnStartedAt?: number;
|
|
85
86
|
terminalTurnIds: Set<string>;
|
|
86
87
|
modeChangeRequestIds: Set<string>;
|
|
88
|
+
planContinuationTurnId?: string;
|
|
87
89
|
finalText: string;
|
|
88
90
|
generatedImagePaths: string[];
|
|
89
91
|
messagePhases: Map<string, "commentary" | "final_answer">;
|
|
@@ -91,7 +93,6 @@ interface CodexSession extends RuntimeSession {
|
|
|
91
93
|
unphasedFinalMessageId?: string;
|
|
92
94
|
needsResume: boolean;
|
|
93
95
|
workspaceSandboxPolicy?: WorkspaceSandboxPolicy;
|
|
94
|
-
canReplaceEmptyThread: boolean;
|
|
95
96
|
settingsRecoveryError?: string;
|
|
96
97
|
rolloutPath?: string;
|
|
97
98
|
turnCount?: number;
|
|
@@ -110,6 +111,10 @@ interface PendingModeChange {
|
|
|
110
111
|
requestId: string;
|
|
111
112
|
targetMode: "plan" | "default";
|
|
112
113
|
responding: boolean;
|
|
114
|
+
decision?: ApprovalDecision;
|
|
115
|
+
automatic?: boolean;
|
|
116
|
+
continued?: boolean;
|
|
117
|
+
terminalEvent?: Extract<RuntimeEvent, { type: "turn_completed" }>;
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
export class CodexRuntime implements AgentRuntime {
|
|
@@ -184,6 +189,20 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
184
189
|
return this.runSessionOperation(input.localSessionId, () => this.resumeSessionNow(input));
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
async prepareSessionForSettings(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
193
|
+
return this.runSessionOperation(input.localSessionId, async () => {
|
|
194
|
+
const existing = this.sessions.get(input.localSessionId);
|
|
195
|
+
if (existing) return existing;
|
|
196
|
+
const session = this.makeSession(input, {
|
|
197
|
+
thread: { id: input.remoteSessionId, name: input.title },
|
|
198
|
+
modelProvider: input.modelProvider, model: input.model,
|
|
199
|
+
}, input.reasoningEffort);
|
|
200
|
+
session.needsResume = true;
|
|
201
|
+
this.sessions.set(input.localSessionId, session);
|
|
202
|
+
return session;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
187
206
|
private async resumeSessionNow(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
188
207
|
const client = await this.client();
|
|
189
208
|
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
@@ -195,8 +214,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
195
214
|
...threadLifecycleParams(input.cwd),
|
|
196
215
|
...permissionParams(input.permissionMode),
|
|
197
216
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
217
|
+
if (response.thread.id !== input.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未恢复会话。");
|
|
198
218
|
const reasoningEffort = await this.resolveReasoningEffort(input, response);
|
|
199
219
|
const session = this.makeSession(input, response, reasoningEffort);
|
|
220
|
+
try {
|
|
221
|
+
assertProviderSettingsApplied(response, { modelProvider: input.modelProvider });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// Keep the attachment available for an explicit settings repair, but never send to the wrong Provider.
|
|
224
|
+
session.settingsRecoveryError = providerSwitchFailure(error);
|
|
225
|
+
session.needsResume = true;
|
|
226
|
+
}
|
|
200
227
|
this.sessions.set(input.localSessionId, session);
|
|
201
228
|
return session;
|
|
202
229
|
}
|
|
@@ -243,13 +270,18 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
243
270
|
const session = this.requireSession(sessionId);
|
|
244
271
|
const client = await this.client();
|
|
245
272
|
await this.ensureSessionResumed(session, client);
|
|
246
|
-
session.canReplaceEmptyThread = false;
|
|
247
273
|
const start = async () => client.request<{ turn: { id: string } }>("turn/start", {
|
|
248
274
|
threadId: session.remoteSessionId,
|
|
249
275
|
input: codexUserInput(prompt),
|
|
250
276
|
cwd: session.cwd,
|
|
251
277
|
model: session.model,
|
|
252
278
|
effort: session.reasoningEffort,
|
|
279
|
+
...(typeof prompt !== "string" && prompt.collaborationMode ? {
|
|
280
|
+
collaborationMode: {
|
|
281
|
+
mode: prompt.collaborationMode,
|
|
282
|
+
settings: { model: session.model, reasoning_effort: session.reasoningEffort ?? null, developer_instructions: null },
|
|
283
|
+
},
|
|
284
|
+
} : {}),
|
|
253
285
|
summary: "auto",
|
|
254
286
|
approvalPolicy: session.permissionMode === "auto" ? "never" : "on-request",
|
|
255
287
|
sandboxPolicy: await this.turnSandboxPolicy(session, client),
|
|
@@ -591,9 +623,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
591
623
|
.find((session) => session.remoteSessionId === remoteSessionId);
|
|
592
624
|
const detectedTurnId = activeThreads?.get(remoteSessionId);
|
|
593
625
|
// A turn started in this process is evidence; a restored activeTurnId alone is not.
|
|
626
|
+
const observedTurnId = runtimeSession?.activeTurnStartedAt !== undefined ? runtimeSession.activeTurnId : undefined;
|
|
594
627
|
const active = status === "active" || Boolean(activeThreads?.has(remoteSessionId))
|
|
595
|
-
|| Boolean(
|
|
596
|
-
const activeTurnId = detectedTurnId ??
|
|
628
|
+
|| Boolean(observedTurnId);
|
|
629
|
+
const activeTurnId = detectedTurnId ?? observedTurnId;
|
|
597
630
|
return {
|
|
598
631
|
active,
|
|
599
632
|
...(active && activeTurnId ? { activeTurnId } : {}),
|
|
@@ -627,6 +660,12 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
627
660
|
}
|
|
628
661
|
|
|
629
662
|
async interruptRemoteTurn(remoteSessionId: string, turnId: string, sessionId?: string): Promise<void> {
|
|
663
|
+
for (const session of this.sessions.values()) {
|
|
664
|
+
if (session.remoteSessionId === remoteSessionId) session.planContinuationTurnId = undefined;
|
|
665
|
+
}
|
|
666
|
+
for (const pending of this.modeChanges.values()) {
|
|
667
|
+
if (pending.turnId === turnId) pending.decision = "cancel";
|
|
668
|
+
}
|
|
630
669
|
await (await this.client()).request(
|
|
631
670
|
"turn/interrupt",
|
|
632
671
|
{ threadId: remoteSessionId, turnId },
|
|
@@ -645,17 +684,51 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
645
684
|
});
|
|
646
685
|
}
|
|
647
686
|
|
|
648
|
-
async archiveRemoteSession(remoteSessionId: string): Promise<void> {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
{ threadId: remoteSessionId },
|
|
652
|
-
|
|
653
|
-
|
|
687
|
+
async archiveRemoteSession(remoteSessionId: string, options: { allowUnmaterialized?: boolean } = {}): Promise<void> {
|
|
688
|
+
const client = await this.client();
|
|
689
|
+
try {
|
|
690
|
+
await client.request("thread/archive", { threadId: remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (!options.allowUnmaterialized || !(error instanceof AppServerRequestError)
|
|
693
|
+
|| error.method !== "thread/archive" || error.code !== -32600
|
|
694
|
+
|| error.serverMessage !== `no rollout found for thread id ${remoteSessionId}`
|
|
695
|
+
|| !await this.isUnmaterializedThread(client, remoteSessionId)) throw error;
|
|
696
|
+
// Codex has nothing to move to its archive before the first user message.
|
|
697
|
+
const result = await client.request<{ status: string }>(
|
|
698
|
+
"thread/unsubscribe", { threadId: remoteSessionId }, CONTROL_REQUEST_TIMEOUT_MS,
|
|
699
|
+
);
|
|
700
|
+
if (!["unsubscribed", "notLoaded", "notSubscribed"].includes(result.status)) {
|
|
701
|
+
throw new Error(`Cannot release empty App Server task: ${result.status}`);
|
|
702
|
+
}
|
|
703
|
+
this.logger.info({ remoteSessionId }, "Released an empty App Server task without a materialized rollout.");
|
|
704
|
+
}
|
|
654
705
|
for (const [localSessionId, session] of this.sessions) {
|
|
655
706
|
if (session.remoteSessionId === remoteSessionId) this.sessions.delete(localSessionId);
|
|
656
707
|
}
|
|
657
708
|
}
|
|
658
709
|
|
|
710
|
+
private async isUnmaterializedThread(client: AppServerClient, remoteSessionId: string): Promise<boolean> {
|
|
711
|
+
const { thread } = await client.request<ThreadReadResponse>(
|
|
712
|
+
"thread/read", { threadId: remoteSessionId, includeTurns: false }, CONTROL_REQUEST_TIMEOUT_MS,
|
|
713
|
+
);
|
|
714
|
+
if (thread.id !== remoteSessionId || thread.preview !== "" || thread.forkedFromId !== null
|
|
715
|
+
|| !["idle", "notLoaded"].includes(thread.status?.type ?? "") || !thread.path
|
|
716
|
+
|| (thread.turns?.length ?? 0) > 0) return false;
|
|
717
|
+
for (const session of this.sessions.values()) {
|
|
718
|
+
if (session.remoteSessionId === remoteSessionId && (session.activeTurnId
|
|
719
|
+
|| session.knownTurnIds.size > 0 || (session.turnCount ?? 0) > 0)) return false;
|
|
720
|
+
}
|
|
721
|
+
const active = await this.localActivityDetector?.activeThreads([remoteSessionId]);
|
|
722
|
+
if (active?.has(remoteSessionId)) return false;
|
|
723
|
+
try {
|
|
724
|
+
statSync(thread.path);
|
|
725
|
+
return false;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
659
732
|
async setTitle(sessionId: string, title: string): Promise<void> {
|
|
660
733
|
return this.runSessionOperation(sessionId, () => this.setTitleNow(sessionId, title));
|
|
661
734
|
}
|
|
@@ -696,7 +769,6 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
696
769
|
const session = this.requireSession(sessionId);
|
|
697
770
|
const client = await this.client();
|
|
698
771
|
await this.ensureSessionResumed(session, client);
|
|
699
|
-
session.canReplaceEmptyThread = false;
|
|
700
772
|
const response = await client.request<{ goal: RuntimeGoal }>(
|
|
701
773
|
"thread/goal/set",
|
|
702
774
|
{ threadId: session.remoteSessionId, ...update },
|
|
@@ -738,7 +810,16 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
738
810
|
settings: RuntimeExecutionSettings,
|
|
739
811
|
persist?: (session: RuntimeSession) => Promise<void>,
|
|
740
812
|
): Promise<RuntimeSession> {
|
|
741
|
-
return this.runSessionOperation(sessionId, () =>
|
|
813
|
+
return this.runSessionOperation(sessionId, async () => {
|
|
814
|
+
try {
|
|
815
|
+
return await this.setExecutionSettingsNow(sessionId, settings, persist);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (error instanceof ProviderSwitchError) throw error;
|
|
818
|
+
// Errors outside the mutation block are preflight failures; no settings were changed.
|
|
819
|
+
const session = this.sessions.get(sessionId);
|
|
820
|
+
throw new ProviderSwitchError(providerSwitchFailure(error), Boolean(session && !session.needsResume && !session.settingsRecoveryError), { cause: error });
|
|
821
|
+
}
|
|
822
|
+
});
|
|
742
823
|
}
|
|
743
824
|
|
|
744
825
|
private async setExecutionSettingsNow(
|
|
@@ -756,82 +837,54 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
756
837
|
if (metadata.thread.status?.type === "active" || localActive?.has(session.remoteSessionId) || session.activeTurnId) {
|
|
757
838
|
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
758
839
|
}
|
|
759
|
-
|
|
760
|
-
if (session.canReplaceEmptyThread && !metadata.thread.forkedFromId && !metadata.thread.preview?.trim()) {
|
|
840
|
+
if (session.turnCount === 0 && !metadata.thread.forkedFromId && !metadata.thread.preview?.trim()) {
|
|
761
841
|
try {
|
|
762
842
|
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
763
|
-
|
|
764
|
-
if (!empty) session.canReplaceEmptyThread = false;
|
|
843
|
+
if (turns.length === 0) throw new Error(PROVIDER_SWITCH_EMPTY);
|
|
765
844
|
} catch (error) {
|
|
766
845
|
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
767
846
|
|| !/missing source rollout|no rollout found/iu.test(error.serverMessage)) throw error;
|
|
768
|
-
|
|
847
|
+
throw new Error(PROVIDER_SWITCH_EMPTY, { cause: error });
|
|
769
848
|
}
|
|
770
849
|
}
|
|
771
850
|
const previous = { ...session };
|
|
772
|
-
|
|
851
|
+
const restoreProvider = previous.modelProvider?.trim()
|
|
852
|
+
|| (metadata.thread.id === previous.remoteSessionId ? metadata.thread.modelProvider?.trim() : undefined);
|
|
773
853
|
let detached = false;
|
|
774
854
|
try {
|
|
775
855
|
const params = {
|
|
776
856
|
cwd: session.cwd, modelProvider: settings.modelProvider, model: settings.model,
|
|
777
857
|
...threadLifecycleParams(session.cwd), ...permissionParams(settings.permissionMode),
|
|
778
858
|
};
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
let response: ThreadResponse;
|
|
784
|
-
if (empty) {
|
|
785
|
-
response = await client.request<ThreadResponse>("thread/start", {
|
|
786
|
-
...params, threadSource: "user", allowProviderModelFallback: false,
|
|
787
|
-
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
788
|
-
replacementId = response.thread.id;
|
|
789
|
-
} else {
|
|
790
|
-
response = await client.request<ThreadResponse>("thread/resume", {
|
|
791
|
-
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
792
|
-
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
793
|
-
if (!providerSettingsMatch(response, settings)) {
|
|
794
|
-
this.logger.warn(
|
|
795
|
-
{
|
|
796
|
-
sessionId,
|
|
797
|
-
threadId: session.remoteSessionId,
|
|
798
|
-
requestedProvider: settings.modelProvider,
|
|
799
|
-
actualProvider: response.modelProvider,
|
|
800
|
-
requestedModel: settings.model,
|
|
801
|
-
actualModel: response.model,
|
|
802
|
-
},
|
|
803
|
-
"App Server did not apply Provider settings while resuming; forking the latest completed Turn.",
|
|
804
|
-
);
|
|
805
|
-
await this.detachThreadForSettings(client, session);
|
|
806
|
-
response = await this.forkThreadForSettings(client, session, settings);
|
|
807
|
-
replacementId = response.thread.id;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
859
|
+
await this.detachThreadForSettings(client, session, () => { detached = true; });
|
|
860
|
+
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
861
|
+
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
862
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
810
863
|
assertProviderSettingsApplied(response, settings);
|
|
811
|
-
if (
|
|
864
|
+
if (response.thread.id !== session.remoteSessionId) {
|
|
812
865
|
throw new Error("App Server 返回了不同的任务 ID,未保存设置。");
|
|
813
866
|
}
|
|
814
|
-
|
|
815
|
-
threadId: response.thread.id, name: session.title,
|
|
816
|
-
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
817
|
-
const candidate = { ...session, ...settings, remoteSessionId: response.thread.id,
|
|
867
|
+
const candidate = { ...session, ...settings,
|
|
818
868
|
workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
|
|
819
869
|
needsResume: false, settingsRecoveryError: undefined };
|
|
820
870
|
await persist?.(candidate);
|
|
821
|
-
Object.assign(session,
|
|
871
|
+
Object.assign(session, settings);
|
|
872
|
+
session.workspaceSandboxPolicy = candidate.workspaceSandboxPolicy;
|
|
873
|
+
session.needsResume = this.attachedClient !== client;
|
|
874
|
+
session.settingsRecoveryError = undefined;
|
|
822
875
|
} catch (error) {
|
|
823
|
-
if (replacementId) await this.discardUnusedThread(client, replacementId);
|
|
824
876
|
let recovery = "原任务设置未更改。";
|
|
825
877
|
if (detached) {
|
|
826
878
|
try {
|
|
879
|
+
if (!restoreProvider) throw new Error("无法确认切换前的 Provider,不能验证原设置已恢复。");
|
|
827
880
|
await this.detachThreadForSettings(client, previous);
|
|
828
881
|
const restored = await client.request<ThreadResponse>("thread/resume", {
|
|
829
882
|
threadId: previous.remoteSessionId, excludeTurns: true, cwd: previous.cwd,
|
|
830
|
-
modelProvider:
|
|
883
|
+
modelProvider: restoreProvider, model: previous.model,
|
|
831
884
|
...threadLifecycleParams(previous.cwd), ...permissionParams(previous.permissionMode),
|
|
832
885
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
833
|
-
|
|
834
|
-
modelProvider:
|
|
886
|
+
assertProviderSettingsApplied(restored, {
|
|
887
|
+
modelProvider: restoreProvider, model: previous.model,
|
|
835
888
|
});
|
|
836
889
|
if (restored.thread.id !== previous.remoteSessionId) throw new Error("恢复时返回了不同的任务 ID。");
|
|
837
890
|
session.needsResume = false;
|
|
@@ -844,60 +897,42 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
844
897
|
session.needsResume = true;
|
|
845
898
|
}
|
|
846
899
|
}
|
|
847
|
-
throw new
|
|
900
|
+
throw new ProviderSwitchError(
|
|
901
|
+
`Provider 切换失败:${providerSwitchFailure(error)} ${recovery}`,
|
|
902
|
+
!session.needsResume && !session.settingsRecoveryError, { cause: error },
|
|
903
|
+
);
|
|
848
904
|
}
|
|
849
|
-
if (replacementId) await this.discardUnusedThread(client, previous.remoteSessionId);
|
|
850
905
|
return session;
|
|
851
906
|
}
|
|
852
907
|
|
|
853
|
-
private async detachThreadForSettings(client: AppServerClient, session: CodexSession): Promise<void> {
|
|
908
|
+
private async detachThreadForSettings(client: AppServerClient, session: CodexSession, onDetach?: () => void): Promise<void> {
|
|
854
909
|
const current = await client.request<ThreadReadResponse>("thread/read", {
|
|
855
910
|
threadId: session.remoteSessionId, includeTurns: false,
|
|
856
911
|
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
857
912
|
if (session.activeTurnId || current.thread.status?.type === "active") throw new Error(PROVIDER_SWITCH_BUSY);
|
|
913
|
+
// A timeout does not cancel the remote unsubscribe, so recovery is required once it is sent.
|
|
914
|
+
onDetach?.();
|
|
858
915
|
const result = await client.request<{ status: string }>("thread/unsubscribe", {
|
|
859
916
|
threadId: session.remoteSessionId,
|
|
860
917
|
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
861
|
-
if (
|
|
862
|
-
throw new Error(
|
|
918
|
+
if (!["unsubscribed", "notLoaded", "notSubscribed"].includes(result.status)) {
|
|
919
|
+
throw new Error(`App Server 返回了未知的取消订阅状态:${result.status}`);
|
|
863
920
|
}
|
|
864
921
|
}
|
|
865
922
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
): Promise<
|
|
871
|
-
|
|
872
|
-
const lastTurnId = turns.find((turn) => turn.status === "completed")?.id;
|
|
873
|
-
if (!lastTurnId) {
|
|
874
|
-
throw new Error("无法为 Provider 切换找到已完成的 Turn。请先完成一轮任务后重试。");
|
|
875
|
-
}
|
|
876
|
-
return client.request<ThreadResponse>("thread/fork", {
|
|
877
|
-
threadId: session.remoteSessionId,
|
|
878
|
-
lastTurnId,
|
|
879
|
-
cwd: session.cwd,
|
|
880
|
-
model: settings.model,
|
|
881
|
-
modelProvider: settings.modelProvider,
|
|
882
|
-
threadSource: "user",
|
|
883
|
-
excludeTurns: true,
|
|
884
|
-
...threadLifecycleParams(session.cwd),
|
|
885
|
-
...permissionParams(settings.permissionMode),
|
|
886
|
-
}, FORK_REQUEST_TIMEOUT_MS);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
private async discardUnusedThread(client: AppServerClient, threadId: string): Promise<void> {
|
|
890
|
-
try {
|
|
891
|
-
await client.request("thread/unsubscribe", { threadId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
892
|
-
} catch (error) {
|
|
893
|
-
this.logger.warn({ error, threadId }, "Failed to release an unused empty Thread after changing Provider.");
|
|
894
|
-
}
|
|
923
|
+
async respondToApproval(
|
|
924
|
+
sessionId: string,
|
|
925
|
+
requestId: string,
|
|
926
|
+
decision: ApprovalDecision,
|
|
927
|
+
): Promise<void> {
|
|
928
|
+
return this.resolveApproval(sessionId, requestId, decision);
|
|
895
929
|
}
|
|
896
930
|
|
|
897
|
-
async
|
|
931
|
+
private async resolveApproval(
|
|
898
932
|
sessionId: string,
|
|
899
933
|
requestId: string,
|
|
900
934
|
decision: ApprovalDecision,
|
|
935
|
+
automatic = false,
|
|
901
936
|
): Promise<void> {
|
|
902
937
|
const modeChange = this.modeChanges.get(requestId);
|
|
903
938
|
if (modeChange) {
|
|
@@ -910,6 +945,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
910
945
|
}
|
|
911
946
|
if (modeChange.responding) throw new Error("Approval response is already in progress.");
|
|
912
947
|
modeChange.responding = true;
|
|
948
|
+
modeChange.decision = decision;
|
|
949
|
+
modeChange.automatic = automatic;
|
|
913
950
|
try {
|
|
914
951
|
const client = await this.client();
|
|
915
952
|
if (this.modeChanges.get(requestId) !== modeChange) throw new Error("Approval request is no longer pending.");
|
|
@@ -919,11 +956,14 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
919
956
|
decision: decision === "accept" ? "approved" : decision === "decline" ? "rejected" : "cancelled",
|
|
920
957
|
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
921
958
|
if (this.modeChanges.get(requestId) === modeChange) {
|
|
959
|
+
this.confirmPlanContinuation(modeChange, decision);
|
|
922
960
|
this.modeChanges.delete(requestId);
|
|
923
|
-
this.emit({ type: "approval_resolved", sessionId, turnId: modeChange.turnId, requestId, decision
|
|
961
|
+
this.emit({ type: "approval_resolved", sessionId, turnId: modeChange.turnId, requestId, decision,
|
|
962
|
+
...(automatic ? { automatic: true } : {}) });
|
|
924
963
|
}
|
|
925
964
|
} finally {
|
|
926
965
|
modeChange.responding = false;
|
|
966
|
+
if (modeChange.terminalEvent) this.emit(modeChange.terminalEvent);
|
|
927
967
|
}
|
|
928
968
|
return;
|
|
929
969
|
}
|
|
@@ -1174,34 +1214,45 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1174
1214
|
const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
|
|
1175
1215
|
if (session.modeChangeRequestIds.has(requestId)) return;
|
|
1176
1216
|
session.modeChangeRequestIds.add(requestId);
|
|
1177
|
-
|
|
1217
|
+
const pending: PendingModeChange = {
|
|
1178
1218
|
sessionId, turnId: mapped.turnId, requestId: mapped.requestId,
|
|
1179
1219
|
targetMode: mapped.targetMode, responding: false,
|
|
1180
|
-
}
|
|
1181
|
-
this.
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1220
|
+
};
|
|
1221
|
+
this.modeChanges.set(requestId, pending);
|
|
1222
|
+
const automatic = session.permissionMode === "auto" && this.provider.getAgentFamily?.() === "traex";
|
|
1223
|
+
const request: ApprovalRequest = {
|
|
1224
|
+
id: requestId,
|
|
1225
|
+
kind: "mode_change",
|
|
1226
|
+
...(automatic ? { automatic: true } : {}),
|
|
1227
|
+
title: mapped.targetMode === "default" ? "确认方案并开始执行" : "确认进入规划模式",
|
|
1228
|
+
reason: [mapped.reason, ...(mapped.allowedPrompts ?? []).map((entry) => `- ${entry.tool}: ${entry.prompt}`)]
|
|
1229
|
+
.filter(Boolean).join("\n"),
|
|
1230
|
+
options: [
|
|
1231
|
+
{ id: "accept", label: mapped.targetMode === "default" ? "Approve Plan" : "Enter Plan Mode" },
|
|
1232
|
+
{ id: "decline", label: "Reject" },
|
|
1233
|
+
{ id: "cancel", label: "Cancel Request" },
|
|
1234
|
+
],
|
|
1235
|
+
};
|
|
1236
|
+
this.emit({ type: "approval_requested", sessionId, turnId: mapped.turnId, request });
|
|
1237
|
+
if (automatic) {
|
|
1238
|
+
void this.resolveApproval(sessionId, requestId, "accept", true).catch((error: unknown) => {
|
|
1239
|
+
this.logger.warn({ error, sessionId, requestId }, "Failed to automatically approve the TraeX mode change.");
|
|
1240
|
+
if (this.modeChanges.get(requestId) !== pending || session.activeTurnId !== mapped.turnId) return;
|
|
1241
|
+
this.emit({ type: "approval_requested", sessionId, turnId: mapped.turnId,
|
|
1242
|
+
request: { ...request, automatic: false, reason: ["自动同意失败,请手动确认。", request.reason].filter(Boolean).join("\n\n") } });
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1196
1245
|
} else if (mapped.kind === "mode_change_resolved") {
|
|
1197
1246
|
const requestId = `mode:${mapped.threadId}:${mapped.requestId}`;
|
|
1198
1247
|
const pending = this.modeChanges.get(requestId);
|
|
1199
1248
|
if (!pending || pending.sessionId !== sessionId || pending.turnId !== mapped.turnId
|
|
1200
1249
|
|| pending.targetMode !== mapped.targetMode) return;
|
|
1250
|
+
this.confirmPlanContinuation(pending, mapped.decision === "approved" ? "accept" : "decline");
|
|
1201
1251
|
this.modeChanges.delete(requestId);
|
|
1202
1252
|
this.emit({
|
|
1203
1253
|
type: "approval_resolved", sessionId, turnId: mapped.turnId, requestId,
|
|
1204
1254
|
decision: mapped.decision === "approved" ? "accept" : mapped.decision === "rejected" ? "decline" : "cancel",
|
|
1255
|
+
...(pending.automatic && mapped.decision === "approved" ? { automatic: true } : {}),
|
|
1205
1256
|
});
|
|
1206
1257
|
} else if (mapped.kind === "token_usage") {
|
|
1207
1258
|
this.emit({
|
|
@@ -1232,6 +1283,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1232
1283
|
if (mapped.phase === "final_answer") this.promoteUnphasedMessage(session, mapped.turnId, mapped.itemId);
|
|
1233
1284
|
else if (session.unphasedFinalMessageId === mapped.itemId) session.unphasedFinalMessageId = undefined;
|
|
1234
1285
|
} else if (mapped.kind === "agent_delta") {
|
|
1286
|
+
this.clearPlanContinuation(session, mapped.turnId);
|
|
1235
1287
|
const phase = session.messagePhases.get(mapped.itemId);
|
|
1236
1288
|
if (phase !== "final_answer") {
|
|
1237
1289
|
// Providers may omit phase. Keep their text in sequence until a tool or completion disambiguates it.
|
|
@@ -1283,6 +1335,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1283
1335
|
} else if (mapped.kind === "plan") {
|
|
1284
1336
|
this.emit({ type: "plan_updated", sessionId, turnId: mapped.turnId, steps: mapped.steps });
|
|
1285
1337
|
} else if (mapped.kind === "tool") {
|
|
1338
|
+
// A backend that resumes tool execution itself needs no client handoff.
|
|
1339
|
+
if (mapped.phase === "started") this.clearPlanContinuation(session, mapped.turnId);
|
|
1286
1340
|
if (mapped.phase === "started") session.unphasedFinalMessageId = undefined;
|
|
1287
1341
|
if (
|
|
1288
1342
|
mapped.phase === "updated"
|
|
@@ -1409,6 +1463,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1409
1463
|
...threadLifecycleParams(session.cwd),
|
|
1410
1464
|
...permissionParams(session.permissionMode),
|
|
1411
1465
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
1466
|
+
if (response.thread.id !== session.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未恢复会话。");
|
|
1467
|
+
assertProviderSettingsApplied(response, { modelProvider: session.modelProvider });
|
|
1412
1468
|
session.workspaceSandboxPolicy = workspaceSandboxFromResponse(response.sandbox);
|
|
1413
1469
|
}
|
|
1414
1470
|
|
|
@@ -1487,13 +1543,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1487
1543
|
}
|
|
1488
1544
|
|
|
1489
1545
|
private adoptTurn(session: CodexSession, turnId: string, startedAt: number): void {
|
|
1490
|
-
session.canReplaceEmptyThread = false;
|
|
1491
1546
|
if (!session.knownTurnIds.has(turnId)) {
|
|
1492
1547
|
session.knownTurnIds.add(turnId);
|
|
1493
1548
|
if (session.turnCount !== undefined) session.turnCount += 1;
|
|
1494
1549
|
}
|
|
1495
1550
|
session.activeTurnId = turnId;
|
|
1496
1551
|
session.activeTurnStartedAt = startedAt;
|
|
1552
|
+
session.planContinuationTurnId = undefined;
|
|
1497
1553
|
session.modeChangeRequestIds.clear();
|
|
1498
1554
|
session.unphasedMessages.clear();
|
|
1499
1555
|
session.unphasedFinalMessageId = undefined;
|
|
@@ -1596,7 +1652,6 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1596
1652
|
unphasedMessages: new Map(),
|
|
1597
1653
|
needsResume: false,
|
|
1598
1654
|
workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
|
|
1599
|
-
canReplaceEmptyThread: !("remoteSessionId" in input),
|
|
1600
1655
|
rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
|
|
1601
1656
|
turnCount: "remoteSessionId" in input ? undefined : 0,
|
|
1602
1657
|
knownTurnIds: new Set(),
|
|
@@ -1626,10 +1681,38 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1626
1681
|
return session;
|
|
1627
1682
|
}
|
|
1628
1683
|
|
|
1684
|
+
private clearPlanContinuation(session: CodexSession, turnId: string): void {
|
|
1685
|
+
session.planContinuationTurnId = undefined;
|
|
1686
|
+
for (const pending of this.modeChanges.values()) {
|
|
1687
|
+
if (pending.sessionId === session.localSessionId && pending.turnId === turnId && pending.responding) {
|
|
1688
|
+
pending.continued = true;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
private confirmPlanContinuation(pending: PendingModeChange, decision: ApprovalDecision): void {
|
|
1694
|
+
const session = this.sessions.get(pending.sessionId);
|
|
1695
|
+
// Do not start work approved in a different client.
|
|
1696
|
+
if (session && pending.responding && !pending.continued && pending.decision === "accept" && decision === "accept"
|
|
1697
|
+
&& pending.targetMode === "default") session.planContinuationTurnId = pending.turnId;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1629
1700
|
private emit(event: RuntimeEvent): void {
|
|
1630
1701
|
if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
|
|
1631
1702
|
for (const [requestId, pending] of this.modeChanges) {
|
|
1632
|
-
if (pending.sessionId
|
|
1703
|
+
if (pending.sessionId !== event.sessionId || pending.turnId !== event.turnId) continue;
|
|
1704
|
+
if (event.type === "turn_completed" && pending.responding && pending.decision === "accept"
|
|
1705
|
+
&& pending.targetMode === "default") {
|
|
1706
|
+
// The terminal notification may beat the approval RPC response.
|
|
1707
|
+
pending.terminalEvent = event;
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
this.modeChanges.delete(requestId);
|
|
1711
|
+
}
|
|
1712
|
+
const session = this.sessions.get(event.sessionId);
|
|
1713
|
+
if (session?.planContinuationTurnId === event.turnId) {
|
|
1714
|
+
session.planContinuationTurnId = undefined;
|
|
1715
|
+
if (event.type === "turn_completed") event = { ...event, approvedPlan: true };
|
|
1633
1716
|
}
|
|
1634
1717
|
}
|
|
1635
1718
|
for (const listener of this.listeners) listener(event);
|
|
@@ -1642,6 +1725,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1642
1725
|
this.modeChanges.clear();
|
|
1643
1726
|
for (const session of this.sessions.values()) {
|
|
1644
1727
|
session.needsResume = true;
|
|
1728
|
+
session.planContinuationTurnId = undefined;
|
|
1645
1729
|
const turnId = session.activeTurnId;
|
|
1646
1730
|
if (!turnId) continue;
|
|
1647
1731
|
session.activeTurnId = undefined;
|
|
@@ -2037,13 +2121,6 @@ function remoteThreadStatus(value: string | undefined): RemoteSessionSummary["st
|
|
|
2037
2121
|
return "not_loaded";
|
|
2038
2122
|
}
|
|
2039
2123
|
|
|
2040
|
-
function providerSettingsMatch(
|
|
2041
|
-
response: Pick<ThreadResponse, "modelProvider" | "model">,
|
|
2042
|
-
settings: RuntimeExecutionSettings,
|
|
2043
|
-
): boolean {
|
|
2044
|
-
return response.modelProvider === settings.modelProvider && response.model === settings.model;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
2124
|
function codexSourceLabel(source: unknown): string {
|
|
2048
2125
|
if (typeof source === "string") return source;
|
|
2049
2126
|
if (isRecord(source)) {
|
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
2
1
|
import type { RuntimeExecutionSettings } from "../runtime/types.js";
|
|
3
2
|
|
|
4
3
|
export const PROVIDER_SWITCH_BUSY = "当前任务正在执行,暂时不能切换 Provider。请等待任务完成,或停止任务后重试。";
|
|
4
|
+
export const PROVIDER_SWITCH_EMPTY = "App Server 尚未保存此空会话,无法在保留任务 ID 的前提下切换 Provider。未改变会话或设置,也不会自动创建替代任务。";
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return /no rollout found|rollout[^\n]*(?:not found|missing)/iu.test(message);
|
|
6
|
+
export class ProviderSwitchError extends Error {
|
|
7
|
+
constructor(message: string, readonly previousSettingsRestored: boolean, options?: ErrorOptions) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = "ProviderSwitchError";
|
|
10
|
+
}
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export function assertProviderSettingsApplied(
|
|
15
14
|
actual: { modelProvider?: string; model?: string },
|
|
16
|
-
requested: Pick<RuntimeExecutionSettings, "modelProvider" | "model"
|
|
15
|
+
requested: Partial<Pick<RuntimeExecutionSettings, "modelProvider" | "model">>,
|
|
17
16
|
): void {
|
|
18
|
-
if (actual.modelProvider !== requested.modelProvider) {
|
|
19
|
-
throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}
|
|
17
|
+
if (requested.modelProvider !== undefined && actual.modelProvider !== requested.modelProvider) {
|
|
18
|
+
throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}。App Server 未应用设置;可在任务空闲后安全重启再重试,不会自动 Fork 或更改历史。`);
|
|
20
19
|
}
|
|
21
|
-
if (actual.model !== requested.model) {
|
|
20
|
+
if (requested.model !== undefined && actual.model !== requested.model) {
|
|
22
21
|
throw new Error(`模型未生效:请求 ${requested.model},实际返回 ${actual.model ?? "未知"}。请确认目标 Provider 支持该模型。`);
|
|
23
22
|
}
|
|
24
23
|
}
|
|
@@ -29,7 +28,7 @@ export function providerSwitchFailure(error: unknown): string {
|
|
|
29
28
|
if (/model[^\n]*(?:unsupported|not supported|not found|unavailable|does not exist)|unknown model/iu.test(message)) {
|
|
30
29
|
return `目标 Provider 不支持当前模型,请选择兼容的模型后重试。${message}`;
|
|
31
30
|
}
|
|
32
|
-
if (/unauthorized|forbidden|auth(?:
|
|
31
|
+
if (/unauthorized|forbidden|auth(?:enticat(?:e|ion)|orization)|api.?key|provider[^\n]*(?:not found|unknown|not configured)/iu.test(message)) {
|
|
33
32
|
return `请检查目标 Provider 的配置和认证信息。${message}`;
|
|
34
33
|
}
|
|
35
34
|
return message;
|
|
@@ -33,7 +33,9 @@ export function writeAgentExecutionDefaults(
|
|
|
33
33
|
for (const key of DEFAULT_KEYS) {
|
|
34
34
|
const value = defaults[key];
|
|
35
35
|
if (value !== undefined) target.set(key, value);
|
|
36
|
+
else target.delete(key);
|
|
36
37
|
}
|
|
38
|
+
if (target.items.length === 0) agent.delete("defaults");
|
|
37
39
|
|
|
38
40
|
const updated = document.toString();
|
|
39
41
|
if (updated === original) return false;
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -30,6 +30,10 @@ updates:
|
|
|
30
30
|
# Daily stable update check, private 60-second cancellation window, then safe update.
|
|
31
31
|
enabled: true
|
|
32
32
|
|
|
33
|
+
terminal:
|
|
34
|
+
enabled: true
|
|
35
|
+
# Shares fileViewer host, port and publicBaseUrl; keeps independent owner authorization.
|
|
36
|
+
|
|
33
37
|
fileViewer:
|
|
34
38
|
enabled: true
|
|
35
39
|
# Use 0.0.0.0 to accept LAN connections and automatically prefer
|