@keyou007/agent-bot 0.1.24-alpha.6 → 0.1.24-alpha.8
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 +24 -0
- package/README.md +6 -4
- package/README.zh.md +6 -4
- package/dist/codex/CodexEventMapper.js +1 -0
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +3 -1
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +70 -8
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/CodexVersion.js +2 -2
- package/dist/codex/CodexVersion.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +1 -1
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/codex/TraePlanEntry.js +70 -0
- package/dist/codex/TraePlanEntry.js.map +1 -0
- package/dist/feishu/CardRenderer.js +27 -1
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +5 -0
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +32 -1
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +3 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/terminal/TerminalCommandTracker.js +74 -0
- package/dist/terminal/TerminalCommandTracker.js.map +1 -0
- package/dist/terminal/TerminalProcess.js +25 -1
- package/dist/terminal/TerminalProcess.js.map +1 -1
- package/dist/terminal/TerminalServer.js +44 -7
- package/dist/terminal/TerminalServer.js.map +1 -1
- package/docs/technical-reference.md +2 -0
- package/docs/technical-reference.zh.md +2 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +6 -4
- package/src/codex/CodexEventMapper.ts +1 -0
- package/src/codex/CodexProcessManager.ts +3 -1
- package/src/codex/CodexRuntime.ts +64 -8
- package/src/codex/CodexVersion.ts +2 -2
- package/src/codex/ProviderSwitch.ts +1 -1
- package/src/codex/TraePlanEntry.ts +63 -0
- package/src/feishu/CardRenderer.ts +30 -2
- package/src/index.ts +14 -3
- package/src/proxy/ProxySessionController.ts +4 -0
- package/src/supervision/SafeRestartNotifier.ts +34 -2
- package/src/supervision/SafeRestartScheduler.ts +7 -0
- package/src/terminal/TerminalCommandTracker.ts +74 -0
- package/src/terminal/TerminalProcess.ts +24 -2
- package/src/terminal/TerminalServer.ts +45 -7
package/package.json
CHANGED
|
@@ -115,7 +115,7 @@ Feishu group renames update only the group's currently bound task title, not top
|
|
|
115
115
|
|
|
116
116
|
Fork creation stores source-task and branch-Turn references without synchronizing the full local Turn list. The Feishu Turn card reuses local records and loads only the summary pages needed for the requested page, not the entire history. `task turns` loads the first page if necessary and returns available local records. This does not change the Agent's inherited context. Retry listing Turns after a temporary history-read failure; unsupported summary pagination never falls back to downloading full history.
|
|
117
117
|
|
|
118
|
-
TraeX plan-mode confirmations follow the task permission mode. With `auto`, Agent Bot automatically accepts Enter Plan Mode and Approve Plan; do not also approve or send a continuation prompt. With `confirm`, `Approve Plan` / `Enter Plan Mode`, `Reject`, and `Cancel Request` require the user's choice. Approval requests and decisions stay in the same thinking card and live Preview, explicitly labeling automatic approvals. If
|
|
118
|
+
TraeX plan-mode confirmations follow the task permission mode. With `auto`, Agent Bot automatically accepts Enter Plan Mode and Approve Plan; do not also approve or send a continuation prompt. With `confirm`, `Approve Plan` / `Enter Plan Mode`, `Reject`, and `Cancel Request` require the user's choice. Approval requests and decisions stay in the same thinking card and live Preview, explicitly labeling automatic approvals. If TraeX reports no pending request after automatically entering Plan mode, Agent Bot verifies the matching completed EnterPlanMode call before recording success. Missing requests alone are not proof of approval. Unverified automatic failures still offer manual confirmation with an error notice; never pretend they succeeded. Approved implementation continues in the same task and card. Explicit discussion-only or no-implementation instructions still apply. Preview cannot approve, cancelling a request does not itself stop the task, and waiting turns still block safe restart. Do not stop or force-restart merely to clear that wait. Missed notifications from older versions are not automatically replayed.
|
|
119
119
|
|
|
120
120
|
## Change Settings
|
|
121
121
|
|
|
@@ -137,9 +137,9 @@ A successful Provider switch confirms settings, not inference connectivity. Mode
|
|
|
137
137
|
|
|
138
138
|
Providers such as a Responses bridge may omit assistant message phases. Unphased text stays in the progress timeline; successful completion promotes only the last message not followed by a tool start. Do not diagnose progress text in the answer area as a CSS issue without checking message phases. Old snapshots are not automatically backfilled.
|
|
139
139
|
|
|
140
|
-
Provider changes during an Agent Bot turn are staged until it ends; do not stop the turn. The settings card and CLI show the pending Provider/model, and model/thinking/permission selections edit that pending configuration. The latest selection is applied before the next turn, including queued/retried turns. Pending settings survive restart, remain separate from actual task settings, and are verified before saving Agent defaults. Deferred failures roll back to the original Provider/model. After the runtime confirms rollback, the card returns to the original settings, the pending selection is cleared, and queued prompts continue with the original settings. Failed or unverified rollback still pauses queued prompts and requires explicit recovery; never claim rollback success based only on unchanged local fields. Custom Providers discover models through their OpenAI-compatible `/models` endpoint when available. Switching keeps the previous model when supported, otherwise uses the Provider default or its first model. A missing model-list endpoint falls back to the current or configured model and does not by itself block switching. Wait for active work to finish; do not stop it without the user's request. Switching
|
|
140
|
+
Provider changes during an Agent Bot turn are staged until it ends; do not stop the turn. The settings card and CLI show the pending Provider/model, and model/thinking/permission selections edit that pending configuration. The latest selection is applied before the next turn, including queued/retried turns. Pending settings survive restart, remain separate from actual task settings, and are verified before saving Agent defaults. Deferred failures roll back to the original Provider/model. After the runtime confirms rollback, the card returns to the original settings, the pending selection is cleared, and queued prompts continue with the original settings. Failed or unverified rollback still pauses queued prompts and requires explicit recovery; never claim rollback success based only on unchanged local fields. Custom Providers discover models through their OpenAI-compatible `/models` endpoint when available. Switching keeps the previous model when supported, otherwise uses the Provider default or its first model. A missing model-list endpoint falls back to the current or configured model and does not by itself block switching. Wait for active work to finish; do not stop it without the user's request. Switching detaches only the selected thread and waits for its actual unload before resuming. On failure, previous settings are retained and recovery is attempted. If recovery fails, resolve the reported configuration or ownership issue and retry the Provider switch before sending another prompt. Never replace a forked task with an empty task to work around a switch failure.
|
|
141
141
|
|
|
142
|
-
Provider changes must keep the same remote task ID and complete history. Never use Fork, thread replacement, synthetic Turns, or history repair to change settings. A rejected switch must report the reason and verify rollback;
|
|
142
|
+
Provider changes must keep the same remote task ID and complete history. Never use Fork, thread replacement, synthetic Turns, or history repair to change settings. A rejected switch must report the reason and verify rollback; Codex is started with zero thread-unload delay, and switching waits for the selected thread to report notLoaded before resuming it. Other tasks may remain active, and empty sibling tasks stay untouched. An unload timeout does not prove external ownership; inspect the selected task and retry after releasing any remaining subscription. Do not restart or force-release the shared process to switch Provider. Settings inspection and selection must remain available when the old Provider cannot resume.
|
|
143
143
|
|
|
144
144
|
## Goals And Turns
|
|
145
145
|
|
|
@@ -207,6 +207,8 @@ The safe-restart card has matching `Cancel` and `ForceRestart` actions. Clicking
|
|
|
207
207
|
|
|
208
208
|
Each displayed running task on that card has a `Stop` action targeting its exact task, even across conversations. It uses normal Agent interruption and leaves the restart scheduled; the card updates as tasks stop. Only stop a task when the user requests it.
|
|
209
209
|
|
|
210
|
+
Blocking terminals are listed on the safe-restart card by their eight-character token, directory, activity/connection state, and current command when available. Activity means a connected foreground page, a known running command, or input/output within 30 seconds; inactive Shells still block restart. PowerShell/PSReadLine reports commands; other Shells may report only a foreground process or unknown. The owner-only End Terminal action stops exactly that Shell and waits for its exit without forcing restart. Never end a user's terminal merely to complete your own restart; require their explicit request. Stale/cancelled cards cannot close terminals.
|
|
211
|
+
|
|
210
212
|
A bare `agentbot server restart` invoked inside an Agent Bot-started Agent returns restart status to the source task conversation. The same command from an ordinary terminal uses the configured user's private chat. Add `--task <task>` only to override either default.
|
|
211
213
|
|
|
212
214
|
Never use `taskkill`, `Stop-Process`, or equivalent commands for routine restart management.
|
|
@@ -221,7 +223,7 @@ agentbot --profile ~/.agent-bot-rescue server status
|
|
|
221
223
|
|
|
222
224
|
Initialization prepares configuration, checks supported Agents, configures Feishu, and starts the server. On first initialization and `init --reset`, let the user choose whether all group messages are accepted or an explicit @ mention is required; only the first choice requests the additional all-group-message permission. Relay authorization links and wait for the user; never choose a response mode, skip authorization, or choose maintenance actions without permission.
|
|
223
225
|
|
|
224
|
-
Codex requires version 0.
|
|
226
|
+
Codex requires version 0.162.1 or later. Init checks this locally, and runtime startup validates the actual App Server version. For an unsupported version, show the upgrade instruction (`codex update` or `npm install -g @openai/codex@latest`); do not work around it by reading full history through legacy APIs. Safely restart Agent Bot after an authorized upgrade. This version requirement does not apply to TraeX or ACP Agents.
|
|
225
227
|
|
|
226
228
|
Use `init --reset` only for an explicitly requested full reset. Without `--profile` it resets the default Profile; pass `--profile <directory>` to reset another Profile. It preserves backups under `.reset-backups`.
|
|
227
229
|
|
|
@@ -4,6 +4,7 @@ import type { PlanStep, ToolState } from "../runtime/types.js";
|
|
|
4
4
|
const modeChangeRequest = z.object({
|
|
5
5
|
requestId: z.string().min(1),
|
|
6
6
|
targetMode: z.enum(["plan", "default"]),
|
|
7
|
+
sourceToolCallId: z.string().min(1).nullish(),
|
|
7
8
|
reason: z.string(),
|
|
8
9
|
allowedPrompts: z.array(z.object({ tool: z.string(), prompt: z.string() })).optional(),
|
|
9
10
|
});
|
|
@@ -60,9 +60,11 @@ export class CodexProcessManager implements AppServerClientProvider {
|
|
|
60
60
|
const environmentContext = this.environmentContext();
|
|
61
61
|
const agentEnvironment = { ...this.env };
|
|
62
62
|
if (this.getAgentFamily() === "codex") agentEnvironment.CODEX_HOME = this.getCodexHome();
|
|
63
|
+
const args = this.getAgentFamily() === "codex"
|
|
64
|
+
? [...this.args, "-c", "thread_unload_delay_secs=0"] : this.args;
|
|
63
65
|
const child = spawnStdioCommand(
|
|
64
66
|
this.command,
|
|
65
|
-
|
|
67
|
+
args,
|
|
66
68
|
agentBotEnvironment({ ...this.codexEnvironment, ...process.env }, agentEnvironment, environmentContext),
|
|
67
69
|
environmentContext.profilePath ?? os.homedir(),
|
|
68
70
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
3
|
import type { Logger } from "pino";
|
|
3
4
|
import type {
|
|
4
5
|
AgentRuntime,
|
|
@@ -34,6 +35,7 @@ import { AppServerRequestError } from "./AppServerConnection.js";
|
|
|
34
35
|
import { workspaceSandboxFromConfig, workspaceSandboxFromResponse, type TurnSandboxPolicy, type WorkspaceSandboxPolicy } from "./AppServerPermissions.js";
|
|
35
36
|
import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
|
|
36
37
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
38
|
+
import { hasCompletedTraePlanEntry } from "./TraePlanEntry.js";
|
|
37
39
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
38
40
|
import { threadWriterLockPath } from "./ThreadWriterProcess.js";
|
|
39
41
|
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, PROVIDER_SWITCH_EMPTY, ProviderSwitchError, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
@@ -110,6 +112,7 @@ interface PendingModeChange {
|
|
|
110
112
|
turnId: string;
|
|
111
113
|
requestId: string;
|
|
112
114
|
targetMode: "plan" | "default";
|
|
115
|
+
sourceToolCallId?: string;
|
|
113
116
|
responding: boolean;
|
|
114
117
|
decision?: ApprovalDecision;
|
|
115
118
|
automatic?: boolean;
|
|
@@ -829,7 +832,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
829
832
|
): Promise<RuntimeSession> {
|
|
830
833
|
const session = this.requireSession(sessionId);
|
|
831
834
|
if (session.activeTurnId) throw new Error(PROVIDER_SWITCH_BUSY);
|
|
832
|
-
|
|
835
|
+
let client = await this.client();
|
|
833
836
|
const metadata = await client.request<ThreadReadResponse>("thread/read", {
|
|
834
837
|
threadId: session.remoteSessionId, includeTurns: false,
|
|
835
838
|
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
@@ -851,12 +854,14 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
851
854
|
const restoreProvider = previous.modelProvider?.trim()
|
|
852
855
|
|| (metadata.thread.id === previous.remoteSessionId ? metadata.thread.modelProvider?.trim() : undefined);
|
|
853
856
|
let detached = false;
|
|
857
|
+
let resumeRequested = false;
|
|
854
858
|
try {
|
|
855
859
|
const params = {
|
|
856
860
|
cwd: session.cwd, modelProvider: settings.modelProvider, model: settings.model,
|
|
857
861
|
...threadLifecycleParams(session.cwd), ...permissionParams(settings.permissionMode),
|
|
858
862
|
};
|
|
859
863
|
await this.detachThreadForSettings(client, session, () => { detached = true; });
|
|
864
|
+
resumeRequested = true;
|
|
860
865
|
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
861
866
|
...params, threadId: session.remoteSessionId, excludeTurns: true,
|
|
862
867
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
@@ -877,7 +882,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
877
882
|
if (detached) {
|
|
878
883
|
try {
|
|
879
884
|
if (!restoreProvider) throw new Error("无法确认切换前的 Provider,不能验证原设置已恢复。");
|
|
880
|
-
await this.
|
|
885
|
+
client = await this.client();
|
|
886
|
+
if (resumeRequested) await this.detachThreadForSettings(client, previous);
|
|
881
887
|
const restored = await client.request<ThreadResponse>("thread/resume", {
|
|
882
888
|
threadId: previous.remoteSessionId, excludeTurns: true, cwd: previous.cwd,
|
|
883
889
|
modelProvider: restoreProvider, model: previous.model,
|
|
@@ -918,6 +924,20 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
918
924
|
if (!["unsubscribed", "notLoaded", "notSubscribed"].includes(result.status)) {
|
|
919
925
|
throw new Error(`App Server 返回了未知的取消订阅状态:${result.status}`);
|
|
920
926
|
}
|
|
927
|
+
if (this.provider.getAgentFamily?.() === "codex") {
|
|
928
|
+
const deadline = Date.now() + CONTROL_REQUEST_TIMEOUT_MS;
|
|
929
|
+
while (Date.now() < deadline) {
|
|
930
|
+
if (this.attachedClient !== client) throw new Error("App Server 连接已变化,请重试 Provider 切换。");
|
|
931
|
+
const { thread } = await client.request<ThreadReadResponse>("thread/read", {
|
|
932
|
+
threadId: session.remoteSessionId, includeTurns: false,
|
|
933
|
+
}, Math.min(SYNC_REQUEST_TIMEOUT_MS, Math.max(1, deadline - Date.now())));
|
|
934
|
+
if (thread.id !== session.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未切换 Provider。");
|
|
935
|
+
if (thread.status?.type === "notLoaded") return;
|
|
936
|
+
if (thread.status?.type === "active" || session.activeTurnId) throw new Error(PROVIDER_SWITCH_BUSY);
|
|
937
|
+
await delay(100);
|
|
938
|
+
}
|
|
939
|
+
throw new Error("等待任务卸载超时;任务可能仍有订阅或正在关闭,请稍后重试 Provider 切换。");
|
|
940
|
+
}
|
|
921
941
|
}
|
|
922
942
|
|
|
923
943
|
async respondToApproval(
|
|
@@ -950,11 +970,15 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
950
970
|
try {
|
|
951
971
|
const client = await this.client();
|
|
952
972
|
if (this.modeChanges.get(requestId) !== modeChange) throw new Error("Approval request is no longer pending.");
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
973
|
+
try {
|
|
974
|
+
await client.request("mode/change/respond", {
|
|
975
|
+
threadId: this.requireSession(sessionId).remoteSessionId,
|
|
976
|
+
requestId: modeChange.requestId,
|
|
977
|
+
decision: decision === "accept" ? "approved" : decision === "decline" ? "rejected" : "cancelled",
|
|
978
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
if (!await this.reconcileCompletedPlanEntry(requestId, modeChange, error)) throw error;
|
|
981
|
+
}
|
|
958
982
|
if (this.modeChanges.get(requestId) === modeChange) {
|
|
959
983
|
this.confirmPlanContinuation(modeChange, decision);
|
|
960
984
|
this.modeChanges.delete(requestId);
|
|
@@ -974,6 +998,36 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
974
998
|
this.emit({ type: "approval_resolved", sessionId, turnId: pending.turnId, requestId, decision });
|
|
975
999
|
}
|
|
976
1000
|
|
|
1001
|
+
private async reconcileCompletedPlanEntry(
|
|
1002
|
+
requestId: string, pending: PendingModeChange, error: unknown,
|
|
1003
|
+
): Promise<boolean> {
|
|
1004
|
+
const session = this.sessions.get(pending.sessionId);
|
|
1005
|
+
if (!pending.automatic || pending.decision !== "accept" || pending.targetMode !== "plan"
|
|
1006
|
+
|| this.provider.getAgentFamily?.() !== "traex" || !session?.rolloutPath
|
|
1007
|
+
|| !(error instanceof AppServerRequestError) || error.method !== "mode/change/respond" || error.code !== -32600
|
|
1008
|
+
|| error.serverMessage !== `failed to respond to mode change request: No pending mode change found for request_id: ${pending.requestId}`) {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
const toolCallId = pending.sourceToolCallId ?? pending.requestId;
|
|
1012
|
+
for (const waitMs of [0, 100, 200]) {
|
|
1013
|
+
if (waitMs) await delay(waitMs);
|
|
1014
|
+
if (this.modeChanges.get(requestId) !== pending || this.sessions.get(pending.sessionId) !== session
|
|
1015
|
+
|| session.activeTurnId !== pending.turnId) return false;
|
|
1016
|
+
try {
|
|
1017
|
+
if (!await hasCompletedTraePlanEntry(session.rolloutPath, pending.turnId, toolCallId)) continue;
|
|
1018
|
+
if (this.modeChanges.get(requestId) !== pending || this.sessions.get(pending.sessionId) !== session
|
|
1019
|
+
|| session.activeTurnId !== pending.turnId) return false;
|
|
1020
|
+
this.logger.info({ sessionId: pending.sessionId, turnId: pending.turnId, requestId, toolCallId },
|
|
1021
|
+
"Verified that TraeX already completed the automatic plan-mode entry.");
|
|
1022
|
+
return true;
|
|
1023
|
+
} catch (verificationError) {
|
|
1024
|
+
this.logger.debug({ error: verificationError, sessionId: pending.sessionId, requestId },
|
|
1025
|
+
"Could not verify TraeX plan-mode entry from the rollout tail.");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
977
1031
|
async listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]> {
|
|
978
1032
|
return (await this.listModelsWithStatus(modelProvider, fallbackModel)).models;
|
|
979
1033
|
}
|
|
@@ -1217,6 +1271,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1217
1271
|
const pending: PendingModeChange = {
|
|
1218
1272
|
sessionId, turnId: mapped.turnId, requestId: mapped.requestId,
|
|
1219
1273
|
targetMode: mapped.targetMode, responding: false,
|
|
1274
|
+
sourceToolCallId: mapped.sourceToolCallId ?? undefined,
|
|
1220
1275
|
};
|
|
1221
1276
|
this.modeChanges.set(requestId, pending);
|
|
1222
1277
|
const automatic = session.permissionMode === "auto" && this.provider.getAgentFamily?.() === "traex";
|
|
@@ -1236,8 +1291,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1236
1291
|
this.emit({ type: "approval_requested", sessionId, turnId: mapped.turnId, request });
|
|
1237
1292
|
if (automatic) {
|
|
1238
1293
|
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
1294
|
if (this.modeChanges.get(requestId) !== pending || session.activeTurnId !== mapped.turnId) return;
|
|
1295
|
+
this.logger.warn({ error, sessionId, requestId, turnId: mapped.turnId, targetMode: mapped.targetMode,
|
|
1296
|
+
sourceToolCallId: pending.sourceToolCallId }, "Failed to automatically approve the TraeX mode change.");
|
|
1241
1297
|
this.emit({ type: "approval_requested", sessionId, turnId: mapped.turnId,
|
|
1242
1298
|
request: { ...request, automatic: false, reason: ["自动同意失败,请手动确认。", request.reason].filter(Boolean).join("\n\n") } });
|
|
1243
1299
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { compareSemanticVersions } from "../utils/semanticVersion.js";
|
|
2
2
|
|
|
3
|
-
// Verified protocol baseline: paginated
|
|
4
|
-
export const MINIMUM_CODEX_VERSION = "0.
|
|
3
|
+
// Verified protocol baseline: paginated history and configurable thread unloading.
|
|
4
|
+
export const MINIMUM_CODEX_VERSION = "0.162.1";
|
|
5
5
|
|
|
6
6
|
export class CodexVersionError extends Error {}
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ export function assertProviderSettingsApplied(
|
|
|
15
15
|
requested: Partial<Pick<RuntimeExecutionSettings, "modelProvider" | "model">>,
|
|
16
16
|
): void {
|
|
17
17
|
if (requested.modelProvider !== undefined && actual.modelProvider !== requested.modelProvider) {
|
|
18
|
-
throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}。App Server
|
|
18
|
+
throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}。App Server 未应用设置,请稍后重试;不会自动 Fork 或更改历史。`);
|
|
19
19
|
}
|
|
20
20
|
if (requested.model !== undefined && actual.model !== requested.model) {
|
|
21
21
|
throw new Error(`模型未生效:请求 ${requested.model},实际返回 ${actual.model ?? "未知"}。请确认目标 Provider 支持该模型。`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const MAX_TAIL_BYTES = 256 * 1024;
|
|
5
|
+
const mutation = z.object({
|
|
6
|
+
type: z.literal("history_mutation"),
|
|
7
|
+
payload: z.object({
|
|
8
|
+
version: z.literal(1),
|
|
9
|
+
turn_id: z.string(),
|
|
10
|
+
operation: z.string(),
|
|
11
|
+
items: z.array(z.object({
|
|
12
|
+
type: z.string(),
|
|
13
|
+
call_id: z.string().optional(),
|
|
14
|
+
name: z.string().optional(),
|
|
15
|
+
output: z.unknown().optional(),
|
|
16
|
+
})).optional(),
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// TraeX can finish EnterPlanMode without a mode/changeResolved notification.
|
|
21
|
+
// Read only a bounded tail, and require the exact call and its successful result.
|
|
22
|
+
export async function hasCompletedTraePlanEntry(
|
|
23
|
+
rolloutPath: string, turnId: string, toolCallId: string,
|
|
24
|
+
): Promise<boolean> {
|
|
25
|
+
const file = await open(rolloutPath, "r");
|
|
26
|
+
try {
|
|
27
|
+
const { size } = await file.stat();
|
|
28
|
+
const start = Math.max(0, size - MAX_TAIL_BYTES);
|
|
29
|
+
const buffer = Buffer.alloc(size - start);
|
|
30
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, start);
|
|
31
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
32
|
+
const lines = text.split("\n");
|
|
33
|
+
if (start > 0) lines.shift();
|
|
34
|
+
lines.pop(); // Ignore an uncommitted/partial final record.
|
|
35
|
+
let entered = false;
|
|
36
|
+
let matchedCall = false;
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
let value: unknown;
|
|
39
|
+
try { value = JSON.parse(line); } catch { continue; }
|
|
40
|
+
const parsed = mutation.safeParse(value);
|
|
41
|
+
if (!parsed.success || parsed.data.payload.turn_id !== turnId) continue;
|
|
42
|
+
const payload = parsed.data.payload;
|
|
43
|
+
if (payload.operation !== "append") {
|
|
44
|
+
matchedCall = false;
|
|
45
|
+
entered = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const item of payload.items ?? []) {
|
|
49
|
+
if (item.call_id !== toolCallId) continue;
|
|
50
|
+
if (item.type === "function_call") {
|
|
51
|
+
matchedCall = item.name === "EnterPlanMode";
|
|
52
|
+
entered = false;
|
|
53
|
+
} else if (item.type === "function_call_output") {
|
|
54
|
+
entered = matchedCall && typeof item.output === "string"
|
|
55
|
+
&& item.output.startsWith("Entered plan mode.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return entered;
|
|
60
|
+
} finally {
|
|
61
|
+
await file.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
16
16
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
17
17
|
import type { UpdateNotice } from "../updates/types.js";
|
|
18
|
-
import type { TerminalCardView } from "../terminal/TerminalServer.js";
|
|
18
|
+
import type { ActiveTerminal, TerminalCardView } from "../terminal/TerminalServer.js";
|
|
19
19
|
import { localCardImage } from "./LocalCardImage.js";
|
|
20
20
|
|
|
21
21
|
export interface StartupStatusView {
|
|
@@ -61,6 +61,8 @@ export interface InitializationWelcomeView {
|
|
|
61
61
|
|
|
62
62
|
export interface SafeRestartStatusView {
|
|
63
63
|
activeTerminals?: number;
|
|
64
|
+
terminalBlockers?: ActiveTerminal[];
|
|
65
|
+
contextKey?: string;
|
|
64
66
|
scheduleId: number;
|
|
65
67
|
reason: string;
|
|
66
68
|
phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting" | "cancelled" | "superseded";
|
|
@@ -1069,9 +1071,35 @@ export class CardRenderer {
|
|
|
1069
1071
|
`**重启原因**:${inlineCode(view.reason)}`,
|
|
1070
1072
|
`**重启倒计时**:${countdown}`,
|
|
1071
1073
|
`**待投递结果**:${view.pendingFinalDeliveries} 条`,
|
|
1072
|
-
...(view.activeTerminals ? [`**活动终端**:${view.activeTerminals}
|
|
1074
|
+
...(view.activeTerminals ? [`**活动终端**:${view.activeTerminals} 个。结束终端会中断其中的命令;ForceRestart 会中断所有终端。`] : []),
|
|
1073
1075
|
];
|
|
1074
1076
|
const elements: Record<string, unknown>[] = [markdown(lines.join("\n"))];
|
|
1077
|
+
if (view.terminalBlockers?.length) {
|
|
1078
|
+
elements.push({ tag: "hr" }, markdown(`**阻止重启的终端(${view.terminalBlockers.length})**`));
|
|
1079
|
+
for (const terminal of view.terminalBlockers) {
|
|
1080
|
+
const command = terminal.state === "closing" ? "正在结束,等待 Shell 退出"
|
|
1081
|
+
: terminal.state === "starting" ? "Shell 启动中"
|
|
1082
|
+
: terminal.command.status === "idle" ? "无(Shell 空闲)"
|
|
1083
|
+
: terminal.command.command ? inlineCode(escapeCardHtml(truncateText(terminal.command.command.replace(/\s+/gu, " "), 240)))
|
|
1084
|
+
: terminal.command.status === "running" ? "命令执行中(命令内容不可用)"
|
|
1085
|
+
: "未知(Shell 未提供命令信息)";
|
|
1086
|
+
const connection = terminal.connected ? terminal.foreground ? "网页前台" : "网页已连接" : "网页已断开";
|
|
1087
|
+
const label = `**终端**:${inlineCode(terminal.token)} · ${terminal.active ? "活跃" : "不活跃"} · ${connection}\n**目录**:${inlineCode(escapeCardHtml(truncateText(terminal.cwd, 240)))}\n**${terminal.command.source === "process" ? "前台进程" : "正在执行"}**:${command}`;
|
|
1088
|
+
elements.push({
|
|
1089
|
+
tag: "column_set", flex_mode: "none", horizontal_spacing: "8px",
|
|
1090
|
+
columns: [
|
|
1091
|
+
{ tag: "column", width: "weighted", weight: 1, elements: [markdown(label)] },
|
|
1092
|
+
...(actionable && terminal.state !== "closing" ? [{
|
|
1093
|
+
tag: "column", width: "auto", elements: [taskActionElement({
|
|
1094
|
+
text: "结束终端", type: "danger",
|
|
1095
|
+
value: { action: "safe_restart_terminal_stop", scheduleId: String(view.scheduleId), terminalId: terminal.terminalId, ...(view.contextKey ? { contextKey: view.contextKey } : {}) },
|
|
1096
|
+
})],
|
|
1097
|
+
}] : []),
|
|
1098
|
+
],
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
elements.push(markdown("活跃:前台连接、正在执行命令或最近 30 秒有输入输出。不活跃的 Shell 仍会阻止安全重启。"));
|
|
1102
|
+
}
|
|
1075
1103
|
if (view.waitingTasks.length > 0) {
|
|
1076
1104
|
const visible = view.waitingTasks.slice(0, 10);
|
|
1077
1105
|
elements.push({ tag: "hr" }, markdown(`**当前等待的任务(${view.waitingTasks.length})**`));
|
package/src/index.ts
CHANGED
|
@@ -145,7 +145,14 @@ if (feishuOutbound) {
|
|
|
145
145
|
workspaceKind: defaultAgent?.kind === "app-server" ? "projectless" : "project",
|
|
146
146
|
defaultUserOpenId: config.feishu.userOpenId,
|
|
147
147
|
}, metadataHydrator);
|
|
148
|
-
safeRestartNotifier = new SafeRestartNotifier(store, feishuOutbound, renderer, logger
|
|
148
|
+
safeRestartNotifier = new SafeRestartNotifier(store, feishuOutbound, renderer, logger, {
|
|
149
|
+
ownerId: () => config.feishu.userOpenId,
|
|
150
|
+
isCurrentSchedule: (scheduleId) => !shuttingDown && !restartRequested && safeRestart.isCurrentSchedule(scheduleId),
|
|
151
|
+
stopTerminal: (terminalId, userId) => {
|
|
152
|
+
if (!terminalServer) throw new Error("网页终端未启用。");
|
|
153
|
+
return terminalServer.stopActiveTerminal(terminalId, userId);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
const consoleEnabled = consoleOnly || config.console.enabled || transport === "console";
|
|
@@ -253,8 +260,8 @@ if (feishuOutbound && localFileViewer && config.terminal?.enabled !== false) {
|
|
|
253
260
|
|
|
254
261
|
function serverActivity() {
|
|
255
262
|
const activity = store.getServerActivityState();
|
|
256
|
-
const
|
|
257
|
-
return { ...activity, activeTerminals };
|
|
263
|
+
const terminalBlockers = terminalServer?.listActiveTerminals() ?? [];
|
|
264
|
+
return { ...activity, activeTerminals: terminalBlockers.length, terminalBlockers };
|
|
258
265
|
}
|
|
259
266
|
|
|
260
267
|
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
@@ -264,6 +271,10 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
|
|
|
264
271
|
if (!terminalServer) throw new Error("网页终端未启用。");
|
|
265
272
|
return terminalServer.resolveApproval(action);
|
|
266
273
|
},
|
|
274
|
+
stopSafeRestartTerminal: (action) => {
|
|
275
|
+
if (!safeRestartNotifier) throw new Error("安全重启通知未启用。");
|
|
276
|
+
return safeRestartNotifier.stopTerminal(action);
|
|
277
|
+
},
|
|
267
278
|
showUpdates: (contextKey, userId) => manualUpdates.show(contextKey, userId),
|
|
268
279
|
selectUpdate: (action, replyTarget) => manualUpdates.select(action, replyTarget),
|
|
269
280
|
restart: requestRestart,
|
|
@@ -515,6 +515,7 @@ export interface ControlTaskReleaseResult {
|
|
|
515
515
|
export interface ProxyLifecycle {
|
|
516
516
|
terminalUrl?(jobId: string): string | undefined;
|
|
517
517
|
authorizeTerminal?(action: CardAction): Promise<string>;
|
|
518
|
+
stopSafeRestartTerminal?(action: CardAction): Promise<string>;
|
|
518
519
|
supervised?: boolean;
|
|
519
520
|
restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
|
|
520
521
|
cancelSafeRestart?(scheduleId: number): Promise<boolean>;
|
|
@@ -1035,6 +1036,9 @@ export class ProxySessionController {
|
|
|
1035
1036
|
if (kind === "terminal_authorize") {
|
|
1036
1037
|
if (!this.lifecycle?.authorizeTerminal) throw new Error("网页终端未启用。");
|
|
1037
1038
|
await this.outbound.sendText(contextKey, await this.lifecycle.authorizeTerminal(scopedAction));
|
|
1039
|
+
} else if (kind === "safe_restart_terminal_stop") {
|
|
1040
|
+
if (!this.lifecycle?.stopSafeRestartTerminal) throw new Error("网页终端未启用。");
|
|
1041
|
+
await this.outbound.sendText(contextKey, await this.lifecycle.stopSafeRestartTerminal(scopedAction));
|
|
1038
1042
|
} else if (kind === "agentbot_update_cancel") {
|
|
1039
1043
|
if (!this.lifecycle?.cancelAutomaticUpdate) throw new Error("自动更新功能未启用。");
|
|
1040
1044
|
await this.lifecycle.cancelAutomaticUpdate(scopedAction);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
2
|
import { CardRenderer } from "../feishu/CardRenderer.js";
|
|
3
3
|
import { isThreadContextKey } from "../feishu/contextKey.js";
|
|
4
|
-
import type { FeishuOutbound } from "../feishu/types.js";
|
|
4
|
+
import type { CardAction, FeishuOutbound } from "../feishu/types.js";
|
|
5
5
|
import type { StateStore } from "../state/StateStore.js";
|
|
6
6
|
import type { RestartNotificationTarget, SafeRestartStatus } from "./SafeRestartScheduler.js";
|
|
7
7
|
|
|
@@ -9,11 +9,15 @@ const DEFAULT_INITIAL_CARD_DELAY_MS = 3_000;
|
|
|
9
9
|
|
|
10
10
|
export interface SafeRestartNotifierOptions {
|
|
11
11
|
initialCardDelayMs?: number;
|
|
12
|
+
ownerId?(): string | undefined;
|
|
13
|
+
isCurrentSchedule?(scheduleId: number): boolean;
|
|
14
|
+
stopTerminal?(terminalId: string, userId: string): Promise<boolean>;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export class SafeRestartNotifier {
|
|
15
18
|
private readonly messageIds = new Map<string, string>();
|
|
16
19
|
private readonly cardHashes = new Map<string, string>();
|
|
20
|
+
private readonly cardTerminalIds = new Map<string, Set<string>>();
|
|
17
21
|
private readonly notificationTargets = new Map<string, RestartNotificationTarget>();
|
|
18
22
|
private activeScheduleId?: number;
|
|
19
23
|
private terminalScheduleId?: number;
|
|
@@ -55,6 +59,7 @@ export class SafeRestartNotifier {
|
|
|
55
59
|
this.activeScheduleId = status.scheduleId;
|
|
56
60
|
this.messageIds.clear();
|
|
57
61
|
this.cardHashes.clear();
|
|
62
|
+
this.cardTerminalIds.clear();
|
|
58
63
|
this.notificationTargets.clear();
|
|
59
64
|
this.clearInitialCardTimer();
|
|
60
65
|
this.pendingInitialStatus = status;
|
|
@@ -90,6 +95,25 @@ export class SafeRestartNotifier {
|
|
|
90
95
|
return [...this.notificationTargets.values()].map((target) => ({ ...target }));
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
async stopTerminal(action: CardAction): Promise<string> {
|
|
99
|
+
const ownerId = this.options.ownerId?.();
|
|
100
|
+
if (!ownerId || action.userId !== ownerId) throw new Error("仅 AgentBot 所有者可结束终端。");
|
|
101
|
+
const scheduleId = Number(action.value.scheduleId);
|
|
102
|
+
const terminalId = typeof action.value.terminalId === "string" ? action.value.terminalId : "";
|
|
103
|
+
if (!Number.isSafeInteger(scheduleId) || scheduleId <= 0 || !/^[a-f0-9]{32}$/u.test(terminalId)) {
|
|
104
|
+
throw new Error("安全重启卡片无效,请使用最新的状态卡片。");
|
|
105
|
+
}
|
|
106
|
+
if (scheduleId !== this.activeScheduleId || !this.options.isCurrentSchedule?.(scheduleId)
|
|
107
|
+
|| !this.lastStatus || ["cancelled", "restarting"].includes(this.lastStatus.phase)
|
|
108
|
+
|| !action.messageId || this.messageIds.get(action.contextKey) !== action.messageId
|
|
109
|
+
|| !this.cardTerminalIds.get(action.contextKey)?.has(terminalId)) {
|
|
110
|
+
throw new Error("该安全重启卡片或终端操作已失效,请查看最新状态卡片。");
|
|
111
|
+
}
|
|
112
|
+
if (!this.options.stopTerminal) throw new Error("网页终端未启用。");
|
|
113
|
+
const stopped = await this.options.stopTerminal(terminalId, ownerId);
|
|
114
|
+
return stopped ? "已请求结束对应终端,安全重启将继续等待其余阻塞项。" : "该终端已结束或正在关闭,无需重复操作。";
|
|
115
|
+
}
|
|
116
|
+
|
|
93
117
|
private publishPendingInitialStatus(): Promise<void> {
|
|
94
118
|
this.clearInitialCardTimer();
|
|
95
119
|
const status = this.pendingInitialStatus;
|
|
@@ -133,6 +157,8 @@ export class SafeRestartNotifier {
|
|
|
133
157
|
remainingMs: status.remainingMs,
|
|
134
158
|
pendingFinalDeliveries: status.activity.pendingFinalDeliveries,
|
|
135
159
|
activeTerminals: status.activity.activeTerminals,
|
|
160
|
+
terminalBlockers: status.activity.terminalBlockers,
|
|
161
|
+
contextKey,
|
|
136
162
|
waitingTasks,
|
|
137
163
|
});
|
|
138
164
|
const cardHash = JSON.stringify(card);
|
|
@@ -141,12 +167,17 @@ export class SafeRestartNotifier {
|
|
|
141
167
|
if (this.cardHashes.get(contextKey) === cardHash) return;
|
|
142
168
|
try {
|
|
143
169
|
await this.outbound.updateInteractiveCard(messageId, card);
|
|
144
|
-
this.
|
|
170
|
+
if (this.activeScheduleId === status.scheduleId && this.messageIds.get(contextKey) === messageId) {
|
|
171
|
+
this.cardHashes.set(contextKey, cardHash);
|
|
172
|
+
this.cardTerminalIds.set(contextKey, new Set((status.activity.terminalBlockers ?? []).map((terminal) => terminal.terminalId)));
|
|
173
|
+
}
|
|
145
174
|
return;
|
|
146
175
|
} catch (error) {
|
|
147
176
|
this.logger.warn({ error, contextKey, messageId }, "Failed to update safe restart status card; sending a replacement.");
|
|
177
|
+
if (this.activeScheduleId !== status.scheduleId) return;
|
|
148
178
|
this.messageIds.delete(contextKey);
|
|
149
179
|
this.cardHashes.delete(contextKey);
|
|
180
|
+
this.cardTerminalIds.delete(contextKey);
|
|
150
181
|
}
|
|
151
182
|
}
|
|
152
183
|
try {
|
|
@@ -170,6 +201,7 @@ export class SafeRestartNotifier {
|
|
|
170
201
|
if (this.activeScheduleId === status.scheduleId) {
|
|
171
202
|
this.messageIds.set(contextKey, created);
|
|
172
203
|
this.cardHashes.set(contextKey, cardHash);
|
|
204
|
+
this.cardTerminalIds.set(contextKey, new Set((status.activity.terminalBlockers ?? []).map((terminal) => terminal.terminalId)));
|
|
173
205
|
} else {
|
|
174
206
|
await this.stopCard(created, status, reason ?? status.reason);
|
|
175
207
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { ActiveTerminal } from "../terminal/TerminalServer.js";
|
|
2
|
+
|
|
1
3
|
export interface ServerActivityState {
|
|
2
4
|
activeTerminals?: number;
|
|
5
|
+
terminalBlockers?: ActiveTerminal[];
|
|
3
6
|
runningSessions: number;
|
|
4
7
|
pendingFinalDeliveries: number;
|
|
5
8
|
latestInboundAt?: string;
|
|
@@ -66,6 +69,10 @@ export class SafeRestartScheduler {
|
|
|
66
69
|
return this.reason;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
isCurrentSchedule(scheduleId: number): boolean {
|
|
73
|
+
return this.scheduled && !this.restartingNow && this.scheduleId === scheduleId;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
schedule(
|
|
70
77
|
reason: string,
|
|
71
78
|
notificationTarget?: RestartNotificationTarget,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export interface TerminalCommandState {
|
|
4
|
+
status: "unknown" | "idle" | "running";
|
|
5
|
+
command?: string;
|
|
6
|
+
source?: "shell" | "process";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class TerminalCommandTracker {
|
|
10
|
+
readonly nonce = randomBytes(16).toString("hex");
|
|
11
|
+
private readonly prefix = String.fromCharCode(27) + "]777;agentbot;" + this.nonce + ";";
|
|
12
|
+
private pending = "";
|
|
13
|
+
private current: TerminalCommandState = { status: "unknown" };
|
|
14
|
+
|
|
15
|
+
snapshot(): TerminalCommandState { return { ...this.current }; }
|
|
16
|
+
|
|
17
|
+
consume(data: string): string {
|
|
18
|
+
this.pending += data;
|
|
19
|
+
let output = "";
|
|
20
|
+
while (this.pending) {
|
|
21
|
+
const start = this.pending.indexOf(this.prefix);
|
|
22
|
+
if (start < 0) {
|
|
23
|
+
let retained = Math.min(this.prefix.length - 1, this.pending.length);
|
|
24
|
+
while (retained && !this.prefix.startsWith(this.pending.slice(-retained))) retained--;
|
|
25
|
+
output += this.pending.slice(0, this.pending.length - retained);
|
|
26
|
+
this.pending = this.pending.slice(this.pending.length - retained);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
output += this.pending.slice(0, start); this.pending = this.pending.slice(start);
|
|
30
|
+
const end = this.pending.indexOf("\u0007", this.prefix.length);
|
|
31
|
+
if (end < 0 && this.pending.length <= 32_768) break;
|
|
32
|
+
if (end < 0 || end > 32_768) {
|
|
33
|
+
output += this.pending[0]; this.pending = this.pending.slice(1); continue;
|
|
34
|
+
}
|
|
35
|
+
const payload = this.pending.slice(this.prefix.length, end);
|
|
36
|
+
if (payload === "idle") this.current = { status: "idle", source: "shell" };
|
|
37
|
+
else if (payload.startsWith("command;") && /^[A-Za-z0-9+/]*={0,2}$/u.test(payload.slice(8))) {
|
|
38
|
+
const command = Buffer.from(payload.slice(8), "base64").toString("utf8")
|
|
39
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/gu, "").trim();
|
|
40
|
+
this.current = { status: "running", source: "shell", command: command.slice(0, 2_000) || undefined };
|
|
41
|
+
} else this.current = { status: "unknown" };
|
|
42
|
+
this.pending = this.pending.slice(end + 1);
|
|
43
|
+
}
|
|
44
|
+
return output;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function powerShellTrackingScript(nonce: string): string {
|
|
49
|
+
if (!/^[a-f0-9]{32}$/u.test(nonce)) throw new Error("Invalid terminal tracking nonce.");
|
|
50
|
+
// Wrap the accepted line, not the history hook (which skips duplicate commands).
|
|
51
|
+
// Leave editing/history policy intact and call the original prompt/readline before altering $?.
|
|
52
|
+
return [
|
|
53
|
+
"$global:__AgentBotPrompt = $function:prompt",
|
|
54
|
+
"$global:__AgentBotMarker = [char]27 + ']777;agentbot;" + nonce + ";'",
|
|
55
|
+
"try {",
|
|
56
|
+
" Import-Module PSReadLine -ErrorAction Stop",
|
|
57
|
+
" $global:__AgentBotReadLine = $function:PSConsoleHostReadLine",
|
|
58
|
+
" if ($null -eq $global:__AgentBotReadLine) { throw 'PSConsoleHostReadLine unavailable' }",
|
|
59
|
+
" function global:PSConsoleHostReadLine {",
|
|
60
|
+
" $line = & $global:__AgentBotReadLine",
|
|
61
|
+
" if (-not [string]::IsNullOrWhiteSpace($line)) {",
|
|
62
|
+
" $text = if ($line.Length -gt 2000) { $line.Substring(0, 2000) } else { $line }",
|
|
63
|
+
" [Console]::Write($global:__AgentBotMarker + 'command;' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($text)) + [char]7)",
|
|
64
|
+
" }",
|
|
65
|
+
" return $line",
|
|
66
|
+
" }",
|
|
67
|
+
" function global:prompt {",
|
|
68
|
+
" $text = & $global:__AgentBotPrompt",
|
|
69
|
+
" [Console]::Write($global:__AgentBotMarker + 'idle' + [char]7)",
|
|
70
|
+
" return $text",
|
|
71
|
+
" }",
|
|
72
|
+
"} catch { [Console]::Write($global:__AgentBotMarker + 'unknown' + [char]7) }",
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|