@linkshell/host 0.4.6 → 0.4.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/dist/host/src/computer-preview-capture.d.ts +11 -0
- package/dist/host/src/computer-preview-capture.js +87 -0
- package/dist/host/src/computer-preview-capture.js.map +1 -0
- package/dist/host/src/computer-preview.d.ts +73 -0
- package/dist/host/src/computer-preview.js +335 -0
- package/dist/host/src/computer-preview.js.map +1 -0
- package/dist/host/src/drivers/acp/connection.d.ts +2 -0
- package/dist/host/src/drivers/acp/connection.js +14 -1
- package/dist/host/src/drivers/acp/connection.js.map +1 -1
- package/dist/host/src/drivers/acp/driver.d.ts +1 -0
- package/dist/host/src/drivers/acp/driver.js +8 -0
- package/dist/host/src/drivers/acp/driver.js.map +1 -1
- package/dist/host/src/drivers/acp/mapper.js +11 -1
- package/dist/host/src/drivers/acp/mapper.js.map +1 -1
- package/dist/host/src/drivers/claude/activity.d.ts +1 -0
- package/dist/host/src/drivers/claude/activity.js +1 -0
- package/dist/host/src/drivers/claude/activity.js.map +1 -1
- package/dist/host/src/drivers/claude/driver.d.ts +6 -0
- package/dist/host/src/drivers/claude/driver.js +30 -2
- package/dist/host/src/drivers/claude/driver.js.map +1 -1
- package/dist/host/src/drivers/claude/tasks.d.ts +33 -0
- package/dist/host/src/drivers/claude/tasks.js +138 -0
- package/dist/host/src/drivers/claude/tasks.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +31 -3
- package/dist/host/src/drivers/claude/transcript.js +20 -1
- package/dist/host/src/drivers/claude/transcript.js.map +1 -1
- package/dist/host/src/drivers/codex/app-server.js +0 -3
- package/dist/host/src/drivers/codex/app-server.js.map +1 -1
- package/dist/host/src/drivers/codex/commands.js +7 -1
- package/dist/host/src/drivers/codex/commands.js.map +1 -1
- package/dist/host/src/drivers/codex/computer-preview.d.ts +3 -0
- package/dist/host/src/drivers/codex/computer-preview.js +44 -0
- package/dist/host/src/drivers/codex/computer-preview.js.map +1 -0
- package/dist/host/src/drivers/codex/driver.d.ts +16 -1
- package/dist/host/src/drivers/codex/driver.js +286 -13
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/dist/host/src/drivers/codex/mapper.d.ts +3 -1
- package/dist/host/src/drivers/codex/mapper.js +77 -7
- package/dist/host/src/drivers/codex/mapper.js.map +1 -1
- package/dist/host/src/drivers/types.d.ts +23 -1
- package/dist/host/src/hub.d.ts +18 -1
- package/dist/host/src/hub.js +99 -1
- package/dist/host/src/hub.js.map +1 -1
- package/dist/host/src/input.d.ts +9 -0
- package/dist/host/src/input.js +15 -0
- package/dist/host/src/input.js.map +1 -1
- package/dist/host/src/rpc/server.d.ts +1 -0
- package/dist/host/src/rpc/server.js +17 -0
- package/dist/host/src/rpc/server.js.map +1 -1
- package/dist/host/src/screen-viewer.js +641 -109
- package/dist/host/src/screen-viewer.js.map +1 -1
- package/dist/host/src/store.d.ts +2 -0
- package/dist/host/src/store.js +4 -0
- package/dist/host/src/store.js.map +1 -1
- package/dist/host/src/task-output.d.ts +6 -0
- package/dist/host/src/task-output.js +22 -0
- package/dist/host/src/task-output.js.map +1 -0
- package/dist/host/tsconfig.tsbuildinfo +1 -1
- package/dist/wire/src/computer-preview.d.ts +35 -0
- package/dist/wire/src/computer-preview.js +51 -0
- package/dist/wire/src/computer-preview.js.map +1 -0
- package/dist/wire/src/goal.d.ts +66 -0
- package/dist/wire/src/goal.js +19 -0
- package/dist/wire/src/goal.js.map +1 -0
- package/dist/wire/src/index.d.ts +2 -0
- package/dist/wire/src/index.js +2 -0
- package/dist/wire/src/index.js.map +1 -1
- package/dist/wire/src/model.d.ts +78 -0
- package/dist/wire/src/model.js +31 -0
- package/dist/wire/src/model.js.map +1 -1
- package/dist/wire/src/rpc.d.ts +1189 -46
- package/dist/wire/src/rpc.js +41 -1
- package/dist/wire/src/rpc.js.map +1 -1
- package/dist/wire/src/updates.d.ts +334 -22
- package/dist/wire/src/updates.js +11 -1
- package/dist/wire/src/updates.js.map +1 -1
- package/package.json +4 -3
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { codexPreview } from "./computer-preview.js";
|
|
3
|
+
import { ABANDON, RpcError, sessionGoalSchema } from "@linkshell/wire";
|
|
3
4
|
import { parseCodexLoginStatus, runStatusCommand } from "../auth.js";
|
|
4
5
|
import { CodexAppServer, CodexSharedServer, detectCodex, sharedServerSocket } from "./app-server.js";
|
|
5
|
-
import { APPROVAL_METHODS, QUESTION_METHODS, mapApprovalRequest, mapQuestionRequest, mapNotification, threadStateOf, threadToDiscovered, spawnedThreads, threadToHistory, toCodexInput, toolStart, turnUnderWay, } from "./mapper.js";
|
|
6
|
+
import { APPROVAL_METHODS, QUESTION_METHODS, mapApprovalRequest, mapQuestionRequest, mapNotification, threadStateOf, threadToDiscovered, spawnedThreads, subagentHistory, threadToHistory, toCodexInput, toolStart, turnUnderWay, } from "./mapper.js";
|
|
6
7
|
import { nestHistory, nestUnder } from "../nesting.js";
|
|
7
8
|
import { configOptions, effective, settingsFrom, turnOverrides } from "./settings.js";
|
|
8
9
|
import { COMMANDS, INIT_PROMPT, commandOf } from "./commands.js";
|
|
@@ -79,6 +80,9 @@ export class CodexDriver {
|
|
|
79
80
|
/** Each thread's working directory: where its skills are looked up. */
|
|
80
81
|
cwds = new Map();
|
|
81
82
|
skills = new Map();
|
|
83
|
+
goalAvailable = new Set();
|
|
84
|
+
goalRevision = new Map();
|
|
85
|
+
skillsRevision = 0;
|
|
82
86
|
models;
|
|
83
87
|
restartTimer;
|
|
84
88
|
restartDelay;
|
|
@@ -93,6 +97,11 @@ export class CodexDriver {
|
|
|
93
97
|
/** Notifications for a thread being joined, held until its history has been read. */
|
|
94
98
|
joining = new Map();
|
|
95
99
|
/** Threads a device has open. */
|
|
100
|
+
taskProcesses = new Map();
|
|
101
|
+
taskPolls = new Map();
|
|
102
|
+
taskSyncs = new Map();
|
|
103
|
+
stoppingTasks = new Set();
|
|
104
|
+
taskRevisions = new Map();
|
|
96
105
|
watching = new Set();
|
|
97
106
|
/** Threads another client of this host's app-server opened (`linkshell codex`): followed for as long as the host runs. */
|
|
98
107
|
kept = new Set();
|
|
@@ -134,6 +143,9 @@ export class CodexDriver {
|
|
|
134
143
|
return parseCodexLoginStatus(await runStatusCommand(this.options.command ?? "codex", ["login", "status"], this.options.env));
|
|
135
144
|
}
|
|
136
145
|
async stop() {
|
|
146
|
+
for (const timer of this.taskPolls.values())
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
this.taskPolls.clear();
|
|
137
149
|
this.stopped = true;
|
|
138
150
|
if (this.restartTimer)
|
|
139
151
|
clearTimeout(this.restartTimer);
|
|
@@ -226,6 +238,7 @@ export class CodexDriver {
|
|
|
226
238
|
// rollout yet, so thread/resume would fail. Nothing to import either.
|
|
227
239
|
void this.announceConfig(nativeId);
|
|
228
240
|
void this.announceCommands(nativeId);
|
|
241
|
+
void this.announceGoal(nativeId);
|
|
229
242
|
return [];
|
|
230
243
|
}
|
|
231
244
|
let joined;
|
|
@@ -250,6 +263,7 @@ export class CodexDriver {
|
|
|
250
263
|
this.reportHeld(nativeId, false);
|
|
251
264
|
void this.announceConfig(nativeId);
|
|
252
265
|
void this.announceCommands(nativeId);
|
|
266
|
+
void this.announceGoal(nativeId);
|
|
253
267
|
if (this.waiting.has(nativeId))
|
|
254
268
|
void this.syncWaiting(nativeId);
|
|
255
269
|
this.considerRelease(nativeId);
|
|
@@ -317,6 +331,7 @@ export class CodexDriver {
|
|
|
317
331
|
}
|
|
318
332
|
/** Says where the joined thread stands, then lets through what arrived while it was being read. */
|
|
319
333
|
goLive(threadId, { thread, underWay }) {
|
|
334
|
+
void this.syncTasks(threadId);
|
|
320
335
|
const state = this.stateOf(threadId);
|
|
321
336
|
const active = thread.turns?.find((turn) => turn.status === "inProgress");
|
|
322
337
|
state.activeTurnId = active?.id;
|
|
@@ -370,6 +385,8 @@ export class CodexDriver {
|
|
|
370
385
|
this.cwds.set(threadId, thread.cwd);
|
|
371
386
|
this.host?.update(this.id, threadId, { sessionUpdate: "ls_status", state: running ? "running" : "idle" });
|
|
372
387
|
this.reportHeld(threadId, true);
|
|
388
|
+
void this.announceCommands(threadId);
|
|
389
|
+
void this.announceGoal(threadId);
|
|
373
390
|
// What was queued for it before this host last started is still waiting.
|
|
374
391
|
void this.syncWaiting(threadId);
|
|
375
392
|
this.considerRelease(threadId);
|
|
@@ -380,10 +397,19 @@ export class CodexDriver {
|
|
|
380
397
|
const watch = this.observed.get(threadId);
|
|
381
398
|
if (!watch || watch.reading)
|
|
382
399
|
return;
|
|
400
|
+
if (!watch.goalReading && this.goalAvailable.has(threadId) && Date.now() - (watch.goalCheckedAt ?? 0) >= 5000) {
|
|
401
|
+
watch.goalCheckedAt = Date.now();
|
|
402
|
+
watch.goalReading = true;
|
|
403
|
+
// Goal changes live in SQLite and need not change the rollout file.
|
|
404
|
+
void this.goal(threadId, { action: "get" }).catch(() => { }).finally(() => { watch.goalReading = false; });
|
|
405
|
+
}
|
|
383
406
|
if (this.waiting.has(threadId))
|
|
384
407
|
void this.syncWaiting(threadId);
|
|
385
408
|
const stamp = watch.path ? fileStamp(watch.path) : undefined;
|
|
386
|
-
|
|
409
|
+
// A parent waiting for its agents may not write anything while their own
|
|
410
|
+
// rollout files keep growing. Each of those files can invalidate the read.
|
|
411
|
+
const childrenChanged = [...this.children.values()].some((child) => child.threadId === threadId && (!child.path || child.stamp === undefined || fileStamp(child.path) !== child.stamp));
|
|
412
|
+
if (stamp !== undefined && stamp === watch.stamp && !childrenChanged)
|
|
387
413
|
return;
|
|
388
414
|
watch.reading = true;
|
|
389
415
|
try {
|
|
@@ -462,6 +488,7 @@ export class CodexDriver {
|
|
|
462
488
|
this.reportHeld(threadId, false);
|
|
463
489
|
void this.announceConfig(threadId);
|
|
464
490
|
void this.announceCommands(threadId);
|
|
491
|
+
void this.announceGoal(threadId);
|
|
465
492
|
// What still waits in Codex's queue is started by this host's app-server now.
|
|
466
493
|
void this.syncWaiting(threadId);
|
|
467
494
|
})().finally(() => this.takingUp.delete(threadId));
|
|
@@ -640,6 +667,7 @@ export class CodexDriver {
|
|
|
640
667
|
watched(nativeId, open) {
|
|
641
668
|
if (open) {
|
|
642
669
|
this.watching.add(nativeId);
|
|
670
|
+
void this.syncTasks(nativeId);
|
|
643
671
|
this.cancelRelease(nativeId);
|
|
644
672
|
}
|
|
645
673
|
else {
|
|
@@ -647,6 +675,84 @@ export class CodexDriver {
|
|
|
647
675
|
this.considerRelease(nativeId);
|
|
648
676
|
}
|
|
649
677
|
}
|
|
678
|
+
publishTask(threadId, task) {
|
|
679
|
+
const { lastSeq: _, ...record } = task;
|
|
680
|
+
this.host?.update(this.id, threadId, { sessionUpdate: "ls_task", task: record });
|
|
681
|
+
}
|
|
682
|
+
loseTasks(threadId) {
|
|
683
|
+
clearTimeout(this.taskPolls.get(threadId));
|
|
684
|
+
this.taskPolls.delete(threadId);
|
|
685
|
+
this.taskProcesses.delete(threadId);
|
|
686
|
+
this.taskRevisions.set(threadId, (this.taskRevisions.get(threadId) ?? 0) + 1);
|
|
687
|
+
for (const task of this.host?.tasks(this.id, threadId) ?? []) {
|
|
688
|
+
if (task.state === "running")
|
|
689
|
+
this.publishTask(threadId, { ...task, state: "unknown", endedAt: Date.now(), canStop: false });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
syncTasks(threadId) {
|
|
693
|
+
const pending = this.taskSyncs.get(threadId);
|
|
694
|
+
if (pending)
|
|
695
|
+
return pending;
|
|
696
|
+
if (!this.attached.has(threadId) || this.observed.has(threadId))
|
|
697
|
+
return Promise.resolve();
|
|
698
|
+
const sync = this.readTasks(threadId).catch(() => { }).finally(() => {
|
|
699
|
+
this.taskSyncs.delete(threadId);
|
|
700
|
+
clearTimeout(this.taskPolls.get(threadId));
|
|
701
|
+
this.taskPolls.delete(threadId);
|
|
702
|
+
if (this.attached.has(threadId) && this.watching.has(threadId) && this.host?.tasks(this.id, threadId).some((task) => task.state === "running")) {
|
|
703
|
+
const timer = setTimeout(() => void this.syncTasks(threadId), 5000);
|
|
704
|
+
timer.unref?.();
|
|
705
|
+
this.taskPolls.set(threadId, timer);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
this.taskSyncs.set(threadId, sync);
|
|
709
|
+
return sync;
|
|
710
|
+
}
|
|
711
|
+
async readTasks(threadId) {
|
|
712
|
+
const revision = this.taskRevisions.get(threadId) ?? 0;
|
|
713
|
+
const entries = [];
|
|
714
|
+
let cursor;
|
|
715
|
+
do {
|
|
716
|
+
const page = await this.rpcFor(threadId, "thread/backgroundTerminals/list", { threadId, limit: 100, ...(cursor ? { cursor } : {}) });
|
|
717
|
+
entries.push(...page.data);
|
|
718
|
+
cursor = page.nextCursor ?? undefined;
|
|
719
|
+
} while (cursor);
|
|
720
|
+
if (!this.attached.has(threadId) || revision !== (this.taskRevisions.get(threadId) ?? 0))
|
|
721
|
+
return;
|
|
722
|
+
const previous = new Map((this.host?.tasks(this.id, threadId) ?? []).map((task) => [task.id, task]));
|
|
723
|
+
const processes = new Map();
|
|
724
|
+
for (const entry of entries) {
|
|
725
|
+
processes.set(entry.itemId, entry.processId);
|
|
726
|
+
const old = previous.get(entry.itemId);
|
|
727
|
+
if (old?.state === "running")
|
|
728
|
+
continue;
|
|
729
|
+
this.publishTask(threadId, { id: entry.itemId, kind: "shell", toolCallId: entry.itemId, title: this.toolTitles.get(entry.itemId) ?? entry.command, command: entry.command, state: "running", startedAt: old?.startedAt ?? Date.now(), output: true, canStop: true });
|
|
730
|
+
}
|
|
731
|
+
this.taskProcesses.set(threadId, processes);
|
|
732
|
+
for (const task of previous.values()) {
|
|
733
|
+
if (task.state === "running" && !processes.has(task.id))
|
|
734
|
+
this.publishTask(threadId, { ...task, state: "unknown", endedAt: Date.now(), canStop: false });
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async stopTask(nativeId, taskId) {
|
|
738
|
+
await this.syncTasks(nativeId);
|
|
739
|
+
const processId = this.taskProcesses.get(nativeId)?.get(taskId);
|
|
740
|
+
if (!processId)
|
|
741
|
+
throw RpcError.app("not_found", "这个后台任务已经不在运行列表里");
|
|
742
|
+
const key = `${nativeId}:${taskId}`;
|
|
743
|
+
this.stoppingTasks.add(key);
|
|
744
|
+
try {
|
|
745
|
+
const result = await this.rpcFor(nativeId, "thread/backgroundTerminals/terminate", { threadId: nativeId, processId });
|
|
746
|
+
if (!result.terminated)
|
|
747
|
+
throw RpcError.app("busy", "没能停止这个后台任务");
|
|
748
|
+
const task = this.host?.tasks(this.id, nativeId).find((task) => task.id === taskId);
|
|
749
|
+
if (task?.state === "running")
|
|
750
|
+
this.publishTask(nativeId, { ...task, state: "stopped", endedAt: Date.now(), canStop: false });
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
this.stoppingTasks.delete(key);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
650
756
|
cancelRelease(threadId) {
|
|
651
757
|
clearTimeout(this.releases.get(threadId));
|
|
652
758
|
this.releases.delete(threadId);
|
|
@@ -707,6 +813,17 @@ export class CodexDriver {
|
|
|
707
813
|
}
|
|
708
814
|
/** Nests each sub-agent's own history right after the call that spawned it. */
|
|
709
815
|
async withSubAgents(threadId, thread, history) {
|
|
816
|
+
// Restore the last surface across idle turns and history windows.
|
|
817
|
+
outer: for (const turn of [...(thread.turns ?? [])].reverse()) {
|
|
818
|
+
for (const item of [...turn.items].reverse()) {
|
|
819
|
+
const stamp = turn.completedAt ?? turn.startedAt ?? thread.updatedAt;
|
|
820
|
+
const frame = codexPreview(item, stamp < 1e12 ? stamp * 1000 : stamp);
|
|
821
|
+
if (frame) {
|
|
822
|
+
this.host?.preview?.(this.id, threadId, frame);
|
|
823
|
+
break outer;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
710
827
|
const spawns = new Map();
|
|
711
828
|
for (const turn of thread.turns ?? []) {
|
|
712
829
|
for (const item of turn.items) {
|
|
@@ -715,17 +832,31 @@ export class CodexDriver {
|
|
|
715
832
|
if (!callId || ids.length === 0)
|
|
716
833
|
continue;
|
|
717
834
|
spawns.set(callId, ids);
|
|
718
|
-
for (const child of ids)
|
|
719
|
-
this.
|
|
835
|
+
for (const child of ids) {
|
|
836
|
+
this.subThreads.add(child);
|
|
837
|
+
this.children.set(child, { ...this.children.get(child), threadId, toolCallId: callId });
|
|
838
|
+
}
|
|
720
839
|
}
|
|
721
840
|
}
|
|
722
841
|
if (spawns.size === 0)
|
|
723
842
|
return history;
|
|
724
843
|
const nested = new Map();
|
|
725
844
|
await Promise.all([...spawns].map(async ([callId, ids]) => {
|
|
726
|
-
const parts = await Promise.all(ids.map((child) =>
|
|
727
|
-
|
|
728
|
-
.
|
|
845
|
+
const parts = await Promise.all(ids.map(async (child) => {
|
|
846
|
+
const known = this.children.get(child);
|
|
847
|
+
const stamp = known.path ? fileStamp(known.path) : undefined;
|
|
848
|
+
try {
|
|
849
|
+
const result = await this.rpcFor(threadId, "thread/read", { threadId: child, includeTurns: true });
|
|
850
|
+
known.path = result.thread.path ?? undefined;
|
|
851
|
+
known.stamp = stamp;
|
|
852
|
+
return nestHistory(subagentHistory(result.thread), callId);
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
// A just-started child may not have written its rollout yet.
|
|
856
|
+
known.stamp = undefined;
|
|
857
|
+
return [];
|
|
858
|
+
}
|
|
859
|
+
}));
|
|
729
860
|
nested.set(callId, parts.flat());
|
|
730
861
|
}));
|
|
731
862
|
return history.flatMap((item) => {
|
|
@@ -735,6 +866,9 @@ export class CodexDriver {
|
|
|
735
866
|
});
|
|
736
867
|
}
|
|
737
868
|
async detach(nativeId) {
|
|
869
|
+
clearTimeout(this.taskPolls.get(nativeId));
|
|
870
|
+
this.taskPolls.delete(nativeId);
|
|
871
|
+
this.taskProcesses.delete(nativeId);
|
|
738
872
|
this.cancelRelease(nativeId);
|
|
739
873
|
this.stopObserving(nativeId);
|
|
740
874
|
if (!this.attached.delete(nativeId))
|
|
@@ -757,6 +891,61 @@ export class CodexDriver {
|
|
|
757
891
|
*/
|
|
758
892
|
async runCommand(nativeId, command, clientMessageId) {
|
|
759
893
|
const echo = () => this.host?.update(this.id, nativeId, { sessionUpdate: "user_message_chunk", messageId: `local-${clientMessageId}`, content: { type: "text", text: command.text } }, `command:${clientMessageId}`);
|
|
894
|
+
if (command.name === "goal") {
|
|
895
|
+
const args = command.args;
|
|
896
|
+
const change = !args ? { action: "get" }
|
|
897
|
+
: args === "pause" || args === "resume" || args === "clear" ? { action: args }
|
|
898
|
+
: { action: "set", objective: args.replace(/^edit\s+/, "") };
|
|
899
|
+
if (args === "edit")
|
|
900
|
+
throw RpcError.app("invalid_params", "请在 /goal edit 后写上新的目标,或打开目标面板编辑");
|
|
901
|
+
const goal = await this.goal(nativeId, change);
|
|
902
|
+
echo();
|
|
903
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_notice", level: "info", title: goal ? `目标:${goal.objective}` : "当前没有目标" });
|
|
904
|
+
return "started";
|
|
905
|
+
}
|
|
906
|
+
if (command.name === "reload-skills") {
|
|
907
|
+
this.skillsRevision++;
|
|
908
|
+
this.skills.delete(this.cwds.get(nativeId) ?? "");
|
|
909
|
+
await this.loadSkills(this.cwds.get(nativeId), true);
|
|
910
|
+
await this.announceCommands(nativeId);
|
|
911
|
+
echo();
|
|
912
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_notice", level: "info", title: "已刷新可用技能" });
|
|
913
|
+
return "started";
|
|
914
|
+
}
|
|
915
|
+
if (["status", "mcp", "apps", "ps", "stop"].includes(command.name)) {
|
|
916
|
+
if (command.args)
|
|
917
|
+
throw RpcError.app("invalid_params", `/${command.name} 暂不接受参数`);
|
|
918
|
+
const lines = [];
|
|
919
|
+
if (command.name === "status") {
|
|
920
|
+
const { thread } = await this.rpcFor(nativeId, "thread/read", { threadId: nativeId, includeTurns: false });
|
|
921
|
+
const current = effective(this.settings.get(nativeId) ?? {}, this.overrides.get(nativeId) ?? {}, await this.loadModels());
|
|
922
|
+
const running = this.observed.get(nativeId)?.running ?? Boolean(this.stateOf(nativeId).activeTurnId);
|
|
923
|
+
lines.push(`目录:${thread.cwd}`, `模型:${current.model ?? "默认"}`, `推理强度:${current.effort ?? "默认"}`, `状态:${running ? "运行中" : "空闲"}`);
|
|
924
|
+
}
|
|
925
|
+
else if (command.name === "stop") {
|
|
926
|
+
await this.rpcFor(nativeId, "thread/backgroundTerminals/clean", { threadId: nativeId });
|
|
927
|
+
lines.push("已停止当前会话的后台终端");
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
const method = command.name === "mcp" ? "mcpServerStatus/list" : command.name === "apps" ? "app/list" : "thread/backgroundTerminals/list";
|
|
931
|
+
let cursor;
|
|
932
|
+
const seen = new Set();
|
|
933
|
+
do {
|
|
934
|
+
const page = await this.rpcFor(nativeId, method, { threadId: nativeId, limit: 100, ...(cursor ? { cursor } : {}) });
|
|
935
|
+
for (const entry of page.data) {
|
|
936
|
+
lines.push(command.name === "ps" ? `${entry.processId ?? ""} · ${entry.command ?? "后台终端"}` : command.name === "mcp" ? `${entry.name ?? "MCP"} · ${Object.keys(entry.tools ?? {}).length} 个工具 · ${entry.authStatus ?? ""}` : `${entry.name ?? "应用"} · ${entry.isEnabled ? "已启用" : "未启用"}`);
|
|
937
|
+
}
|
|
938
|
+
cursor = page.nextCursor ?? undefined;
|
|
939
|
+
if (cursor && seen.has(cursor))
|
|
940
|
+
throw RpcError.app("invalid_params", "Agent 返回了重复的分页游标,请重试");
|
|
941
|
+
if (cursor)
|
|
942
|
+
seen.add(cursor);
|
|
943
|
+
} while (cursor);
|
|
944
|
+
}
|
|
945
|
+
echo();
|
|
946
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_notice", level: "info", title: `/${command.name}`, detail: lines.join("\n") || "当前没有记录" });
|
|
947
|
+
return "started";
|
|
948
|
+
}
|
|
760
949
|
if (command.name === "compact" || command.name === "review") {
|
|
761
950
|
// Not something a queue can carry: they run in the app-server that has the thread.
|
|
762
951
|
if (this.observed.has(nativeId))
|
|
@@ -783,7 +972,7 @@ export class CodexDriver {
|
|
|
783
972
|
}
|
|
784
973
|
const skill = (await this.loadSkills(this.cwds.get(nativeId))).find((entry) => entry.name === command.name);
|
|
785
974
|
if (!skill)
|
|
786
|
-
|
|
975
|
+
throw RpcError.app("not_supported", `当前 Codex 未提供 /${command.name}。请从命令面板选择可用命令。`);
|
|
787
976
|
// The way the TUI sends a skill: the skill itself, and `$name` in the text.
|
|
788
977
|
return this.send(nativeId, [
|
|
789
978
|
{ type: "skill", name: skill.name, path: skill.path },
|
|
@@ -820,6 +1009,41 @@ export class CodexDriver {
|
|
|
820
1009
|
});
|
|
821
1010
|
return "started";
|
|
822
1011
|
}
|
|
1012
|
+
async goal(nativeId, change) {
|
|
1013
|
+
if (this.observed.has(nativeId) && change.action !== "get")
|
|
1014
|
+
throw this.refused(nativeId, "会话开在电脑的另一个 Codex 里 · 暂时只能查看目标", HELD_COMMAND);
|
|
1015
|
+
const method = change.action === "get" ? "thread/goal/get" : change.action === "clear" ? "thread/goal/clear" : "thread/goal/set";
|
|
1016
|
+
let status = "active";
|
|
1017
|
+
if (change.action === "set") {
|
|
1018
|
+
const current = await this.rpcFor(nativeId, "thread/goal/get", { threadId: nativeId });
|
|
1019
|
+
// Editing a paused goal must not silently restart work. This matches the
|
|
1020
|
+
// native editor; finished or exhausted goals start afresh when edited.
|
|
1021
|
+
if (current.goal && ["paused", "blocked", "usageLimited"].includes(current.goal.status))
|
|
1022
|
+
status = current.goal.status;
|
|
1023
|
+
}
|
|
1024
|
+
const params = change.action === "set"
|
|
1025
|
+
? { threadId: nativeId, objective: change.objective, status, ...(change.tokenBudget === undefined ? {} : { tokenBudget: change.tokenBudget }) }
|
|
1026
|
+
: { threadId: nativeId, ...(change.action === "pause" ? { status: "paused" } : change.action === "resume" ? { status: "active" } : {}) };
|
|
1027
|
+
const revision = this.goalRevision.get(nativeId) ?? 0;
|
|
1028
|
+
const result = await this.rpcFor(nativeId, method, params);
|
|
1029
|
+
const goal = change.action === "clear" || result.goal == null ? null : sessionGoalSchema.parse(result.goal);
|
|
1030
|
+
this.goalAvailable.add(nativeId);
|
|
1031
|
+
// A live update can overtake a snapshot request. Never overwrite it with
|
|
1032
|
+
// the older response (especially a null read from just before /goal set).
|
|
1033
|
+
if ((this.goalRevision.get(nativeId) ?? 0) === revision && this.isNews(`goal:${nativeId}`, goal))
|
|
1034
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal });
|
|
1035
|
+
return goal;
|
|
1036
|
+
}
|
|
1037
|
+
async announceGoal(nativeId) {
|
|
1038
|
+
try {
|
|
1039
|
+
await this.goal(nativeId, { action: "get" });
|
|
1040
|
+
await this.announceCommands(nativeId);
|
|
1041
|
+
}
|
|
1042
|
+
catch (error) {
|
|
1043
|
+
// Older Codex builds do not implement Goals; ordinary sessions still work.
|
|
1044
|
+
this.host?.log(`[codex] goal unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
823
1047
|
/** Model, reasoning effort and permissions apply from the next turn on. */
|
|
824
1048
|
async setConfig(nativeId, optionId, value) {
|
|
825
1049
|
const models = await this.loadModels();
|
|
@@ -880,15 +1104,18 @@ export class CodexDriver {
|
|
|
880
1104
|
this.announced.set(key, text);
|
|
881
1105
|
return true;
|
|
882
1106
|
}
|
|
883
|
-
loadSkills(cwd) {
|
|
1107
|
+
loadSkills(cwd, forceReload = false) {
|
|
884
1108
|
const key = cwd ?? "";
|
|
885
1109
|
let loading = this.skills.get(key);
|
|
886
1110
|
if (!loading) {
|
|
887
|
-
loading = this.rpc("skills/list", cwd ? { cwds: [cwd] } : {})
|
|
1111
|
+
loading = this.rpc("skills/list", { ...(cwd ? { cwds: [cwd] } : {}), ...(forceReload ? { forceReload } : {}) })
|
|
888
1112
|
.then((result) => result.data.flatMap((entry) => entry.skills ?? []).filter((skill) => skill.enabled !== false && skill.name && skill.path))
|
|
889
1113
|
.catch((error) => {
|
|
890
|
-
this.skills.
|
|
1114
|
+
if (this.skills.get(key) === loading)
|
|
1115
|
+
this.skills.delete(key);
|
|
891
1116
|
this.host?.log(`[codex] skills/list failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1117
|
+
if (forceReload)
|
|
1118
|
+
throw error;
|
|
892
1119
|
return [];
|
|
893
1120
|
});
|
|
894
1121
|
this.skills.set(key, loading);
|
|
@@ -897,10 +1124,19 @@ export class CodexDriver {
|
|
|
897
1124
|
}
|
|
898
1125
|
/** What `/` offers on a device: the commands above, then the skills this project can use. */
|
|
899
1126
|
async announceCommands(nativeId) {
|
|
1127
|
+
// Built-ins must be usable even while skill discovery is slow or unavailable.
|
|
1128
|
+
if (!this.announced.has(`commands:${nativeId}`)) {
|
|
1129
|
+
this.isNews(`commands:${nativeId}`, COMMANDS);
|
|
1130
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "available_commands_update", availableCommands: COMMANDS });
|
|
1131
|
+
}
|
|
1132
|
+
const revision = this.skillsRevision;
|
|
900
1133
|
const skills = await this.loadSkills(this.cwds.get(nativeId));
|
|
1134
|
+
if (revision !== this.skillsRevision)
|
|
1135
|
+
return;
|
|
901
1136
|
const taken = new Set(COMMANDS.map((command) => command.name));
|
|
902
1137
|
const availableCommands = [
|
|
903
1138
|
...COMMANDS,
|
|
1139
|
+
...(this.goalAvailable.has(nativeId) ? [{ name: "goal", description: "设置、查看、暂停、继续或清除持续目标", hint: "目标 / pause / resume / clear" }] : []),
|
|
904
1140
|
...skills
|
|
905
1141
|
.filter((skill) => !taken.has(skill.name))
|
|
906
1142
|
.map((skill) => ({
|
|
@@ -1044,6 +1280,7 @@ export class CodexDriver {
|
|
|
1044
1280
|
// (What is joined in Codex's background server is still there.)
|
|
1045
1281
|
if (this.inShared.has(threadId))
|
|
1046
1282
|
continue;
|
|
1283
|
+
this.loseTasks(threadId);
|
|
1047
1284
|
this.attached.delete(threadId);
|
|
1048
1285
|
this.threads.delete(threadId);
|
|
1049
1286
|
this.host?.update(this.id, threadId, { sessionUpdate: "ls_status", state: "offline" });
|
|
@@ -1082,6 +1319,23 @@ export class CodexDriver {
|
|
|
1082
1319
|
return state;
|
|
1083
1320
|
}
|
|
1084
1321
|
onNotification(method, params, from) {
|
|
1322
|
+
if (method === "skills/changed") {
|
|
1323
|
+
this.skillsRevision++;
|
|
1324
|
+
this.skills.clear();
|
|
1325
|
+
for (const id of this.attached)
|
|
1326
|
+
void this.announceCommands(id);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (method === "thread/goal/updated" || method === "thread/goal/cleared") {
|
|
1330
|
+
const id = params?.threadId;
|
|
1331
|
+
if (id) {
|
|
1332
|
+
this.goalRevision.set(id, (this.goalRevision.get(id) ?? 0) + 1);
|
|
1333
|
+
if (this.attached.has(id) && !this.goalAvailable.has(id)) {
|
|
1334
|
+
this.goalAvailable.add(id);
|
|
1335
|
+
void this.announceCommands(id);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1085
1339
|
if (method === "thread/deleted") {
|
|
1086
1340
|
// Deleted in the TUI or another client.
|
|
1087
1341
|
const threadId = params?.threadId;
|
|
@@ -1122,10 +1376,26 @@ export class CodexDriver {
|
|
|
1122
1376
|
return;
|
|
1123
1377
|
}
|
|
1124
1378
|
this.noteSpawns(params);
|
|
1379
|
+
if (method === "item/completed" && typeof threadId === "string") {
|
|
1380
|
+
const item = params.item;
|
|
1381
|
+
const task = this.host?.tasks(this.id, threadId).find((task) => task.id === item?.id);
|
|
1382
|
+
if (task && task.state === "running") {
|
|
1383
|
+
this.taskRevisions.set(threadId, (this.taskRevisions.get(threadId) ?? 0) + 1);
|
|
1384
|
+
const stopped = this.stoppingTasks.delete(`${threadId}:${task.id}`);
|
|
1385
|
+
this.publishTask(threadId, { ...task, state: stopped ? "stopped" : item?.exitCode === 0 ? "completed" : item?.exitCode != null || item?.status === "failed" ? "failed" : "unknown", endedAt: Date.now(), exitCode: item?.exitCode ?? undefined, canStop: false });
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (method === "item/completed" && typeof threadId === "string") {
|
|
1389
|
+
const frame = codexPreview(params.item);
|
|
1390
|
+
if (frame)
|
|
1391
|
+
this.host?.preview?.(this.id, threadId, frame);
|
|
1392
|
+
}
|
|
1125
1393
|
for (const mapped of mapNotification(method, params, (id) => this.stateOf(id))) {
|
|
1126
1394
|
const update = from === "shared" && mapped.update.sessionUpdate === "ls_permission_resolved"
|
|
1127
1395
|
? { ...mapped.update, requestId: sharedRequestId(mapped.update.requestId) }
|
|
1128
1396
|
: mapped.update;
|
|
1397
|
+
if (update.sessionUpdate === "ls_goal" && !this.isNews(`goal:${mapped.threadId}`, update.goal))
|
|
1398
|
+
continue;
|
|
1129
1399
|
if (update.sessionUpdate === "tool_call")
|
|
1130
1400
|
this.rememberTitle(update.toolCallId, update.title);
|
|
1131
1401
|
if (update.sessionUpdate === "ls_permission_resolved") {
|
|
@@ -1154,8 +1424,10 @@ export class CodexDriver {
|
|
|
1154
1424
|
continue;
|
|
1155
1425
|
if (update.state === "started")
|
|
1156
1426
|
this.fresh.delete(mapped.threadId);
|
|
1157
|
-
else
|
|
1427
|
+
else {
|
|
1428
|
+
void this.syncTasks(mapped.threadId);
|
|
1158
1429
|
this.considerRelease(mapped.threadId);
|
|
1430
|
+
}
|
|
1159
1431
|
}
|
|
1160
1432
|
}
|
|
1161
1433
|
/** Remembers which spawnAgent call each new sub-agent thread belongs to. */
|
|
@@ -1230,6 +1502,7 @@ export class CodexDriver {
|
|
|
1230
1502
|
const joined = [...this.inShared].filter((threadId) => this.attached.delete(threadId));
|
|
1231
1503
|
this.inShared.clear();
|
|
1232
1504
|
for (const threadId of joined) {
|
|
1505
|
+
this.loseTasks(threadId);
|
|
1233
1506
|
const turnId = this.stateOf(threadId).activeTurnId;
|
|
1234
1507
|
this.threads.delete(threadId);
|
|
1235
1508
|
// Whether the turn goes on is told again when the thread is joined again.
|