tide-commander 1.152.0 → 1.154.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/assets/{BossLogsModal-DZWUfQXs.js → BossLogsModal-Bo0pUCWU.js} +1 -1
- package/dist/assets/{BossSpawnModal-7IwzcqE7.js → BossSpawnModal-BOOHaqaD.js} +1 -1
- package/dist/assets/{ControlsModal-D8L1xD-H.js → ControlsModal-DItGV-CW.js} +1 -1
- package/dist/assets/{DockerLogsModal-_zLe5UFl.js → DockerLogsModal-DjAZ3GL7.js} +1 -1
- package/dist/assets/{EmbeddedEditor-gbiFXvt_.js → EmbeddedEditor-Cihkr8Xb.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-CcXTV3NP.js → GmailOAuthSetup-G1XqiM44.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-wbETidgv.js → GoogleOAuthSetup-Ce6gojdm.js} +1 -1
- package/dist/assets/{IframeModal-a9E5UDBk.js → IframeModal-CSSZJzPv.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BbGyluL0.js → IntegrationsPanel-EspPH09n.js} +2 -2
- package/dist/assets/LogViewerModal-Bn-HDysM.js +1 -0
- package/dist/assets/{MonitoringModal-DymqjenY.js → MonitoringModal-DuSLTKVR.js} +1 -1
- package/dist/assets/{PM2LogsModal-CEaCk-dt.js → PM2LogsModal-Bo2OtaQd.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-q_FFv7L6.js → RestoreArchivedAreaModal-BOx-V6tl.js} +1 -1
- package/dist/assets/{Scene2DCanvas-DOvVtiYO.js → Scene2DCanvas-BIUbi14G.js} +1 -1
- package/dist/assets/{SceneManager-D0NdrG8K.js → SceneManager-CIWrGc-K.js} +4 -4
- package/dist/assets/{SkillsPanel-DpqD60Hy.js → SkillsPanel-_KHtTDhC.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-BdoALd_g.js → SlackMultiInstanceSetup-DbQ3rUtd.js} +1 -1
- package/dist/assets/{SpawnModal-DFFFH2kc.js → SpawnModal-CYEoOMPW.js} +1 -1
- package/dist/assets/{StatisticsModal-Dv_rYZDt.js → StatisticsModal-BJ-coeis.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-CRCCI686.js → SubordinateAssignmentModal-5Z3jvbM9.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-BM2yZ5nr.woff2 +0 -0
- package/dist/assets/{TriggerManagerPanel-DBN8IRa-.js → TriggerManagerPanel-DvC0xP9r.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-uQEvNTs2.js → WorkflowEditorPanel-6ym1_csr.js} +1 -1
- package/dist/assets/{area-logos-CuVxToGM.js → area-logos-mWPCtCnf.js} +1 -1
- package/dist/assets/{index-Dwll5Eyj.js → index-8VVbTmtb.js} +1 -1
- package/dist/assets/{index-BNzYPyBm.css → index-BPjQZacc.css} +1 -1
- package/dist/assets/index-C34l72MW.js +1 -0
- package/dist/assets/{index-DKnfLe7S.js → index-CS8OutmQ.js} +1 -1
- package/dist/assets/{index-ipuWrg1S.js → index-CT2vtbed.js} +3 -3
- package/dist/assets/{index-Blc4MwKs.js → index-CY_NzVPm.js} +1 -1
- package/dist/assets/index-CiKbhMfN.js +1 -0
- package/dist/assets/{index-HvyFhszc.js → index-CnESn83U.js} +2 -2
- package/dist/assets/{index-8Ghh3AQ-.js → index-DZiNmiyC.js} +1 -1
- package/dist/assets/{index-D4iie3Vd.js → index-DxyZ-rYT.js} +1 -1
- package/dist/assets/main-BHBs2E1L.css +1 -0
- package/dist/assets/main-D5j8Ul0m.js +289 -0
- package/dist/assets/{web-D0JSbYeo.js → web-BvAQVbIF.js} +1 -1
- package/dist/assets/{web-1pfPwxBS.js → web-Cd0-XOkz.js} +1 -1
- package/dist/assets/{web-DjgiLxvi.js → web-DMxl9AVF.js} +1 -1
- package/dist/build-info.json +1 -0
- package/dist/index.html +2 -2
- package/dist/locales/de/config.json +1 -0
- package/dist/locales/en/config.json +6 -1
- package/dist/locales/en/terminal.json +36 -0
- package/dist/locales/es/config.json +4 -1
- package/dist/locales/es/terminal.json +8 -0
- package/dist/locales/fr/config.json +1 -0
- package/dist/locales/hi/config.json +1 -0
- package/dist/locales/it/config.json +1 -0
- package/dist/locales/ja/config.json +1 -0
- package/dist/locales/pt/config.json +1 -0
- package/dist/locales/ru/config.json +1 -0
- package/dist/locales/zh-CN/config.json +1 -0
- package/dist/src/packages/server/codex/app-server/app-server-event-adapter.js +222 -0
- package/dist/src/packages/server/codex/app-server/app-server-process.js +304 -0
- package/dist/src/packages/server/codex/app-server/app-server-recovery-store.js +76 -0
- package/dist/src/packages/server/codex/app-server/codex-app-server-runner.js +453 -0
- package/dist/src/packages/server/routes/agents.js +29 -1
- package/dist/src/packages/server/routes/system.js +216 -1
- package/dist/src/packages/server/runtime/codex-runner-router.js +99 -0
- package/dist/src/packages/server/runtime/codex-runtime-provider.js +5 -3
- package/dist/src/packages/server/services/claude-credentials-service.js +536 -0
- package/dist/src/packages/server/services/claude-usage-service.js +35 -28
- package/dist/src/packages/server/services/provider-credentials-service.js +338 -0
- package/dist/src/packages/server/services/system-prompt-service.js +46 -0
- package/package.json +1 -1
- package/dist/assets/LogViewerModal-cnMAbNvY.js +0 -1
- package/dist/assets/index-BHM8V4i-.js +0 -1
- package/dist/assets/index-OkenGYC6.js +0 -1
- package/dist/assets/main-DpNJ8pqp.css +0 -1
- package/dist/assets/main-aIpEYDDR.js +0 -289
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexAppServerRunner — experimental runtime that drives Codex agents through a
|
|
3
|
+
* persistent, restart-surviving `codex app-server` (WebSocket transport) instead
|
|
4
|
+
* of one `codex exec` process per turn.
|
|
5
|
+
*
|
|
6
|
+
* Why it exists: `codex exec --experimental-json` emits the whole assistant
|
|
7
|
+
* reply as a single `item.completed`, so Codex replies can never typewriter.
|
|
8
|
+
* app-server streams `item/agentMessage/delta` tokens.
|
|
9
|
+
*
|
|
10
|
+
* Persistence: the app-server is spawned DETACHED on a localhost WebSocket, so
|
|
11
|
+
* it outlives commander restarts and keeps running any in-flight turn. On boot
|
|
12
|
+
* the runner rejoins the live daemon and `thread/resume` re-subscribes to each
|
|
13
|
+
* agent's thread — the server even replays the turn's remaining deltas. State
|
|
14
|
+
* needed to reconnect is persisted in a dedicated recovery file.
|
|
15
|
+
*
|
|
16
|
+
* Downstream reuse: reconstructed StandardEvents are fed into a shared
|
|
17
|
+
* RunnerStdoutPipeline via `emitStandardEvent` (the CodexBackend's
|
|
18
|
+
* `.name === 'codex'` turns on the pipeline's stream-coalescing), so websocket
|
|
19
|
+
* broadcast, client merge-by-uuid, boss delegation and context tracking are all
|
|
20
|
+
* reused unchanged from the exec path.
|
|
21
|
+
*/
|
|
22
|
+
import * as os from 'os';
|
|
23
|
+
import { CodexBackend, buildCodexPrompt } from '../backend.js';
|
|
24
|
+
import { RunnerInternalEventBus } from '../../claude/runner/internal-events.js';
|
|
25
|
+
import { RunnerStdoutPipeline } from '../../claude/runner/stdout-pipeline.js';
|
|
26
|
+
import { CodexAppServerProcess } from './app-server-process.js';
|
|
27
|
+
import { CodexAppServerEventAdapter } from './app-server-event-adapter.js';
|
|
28
|
+
import { loadAppServerAgents, saveAppServerAgents, } from './app-server-recovery-store.js';
|
|
29
|
+
import * as agentService from '../../services/agent-service.js';
|
|
30
|
+
import { createLogger } from '../../utils/logger.js';
|
|
31
|
+
import { withAgentContext } from '../../utils/log-context.js';
|
|
32
|
+
const log = createLogger('CodexAppServerRunner');
|
|
33
|
+
/** Defer recovery slightly so the rest of the server is up before we replay. */
|
|
34
|
+
const RECOVER_DELAY_MS = 1500;
|
|
35
|
+
export class CodexAppServerRunner {
|
|
36
|
+
callbacks;
|
|
37
|
+
backend = new CodexBackend();
|
|
38
|
+
bus = new RunnerInternalEventBus();
|
|
39
|
+
pipeline;
|
|
40
|
+
agents = new Map();
|
|
41
|
+
threadToAgent = new Map();
|
|
42
|
+
activityCallbacks = new Map();
|
|
43
|
+
process = null;
|
|
44
|
+
started = false;
|
|
45
|
+
/** Guard so run/stop during the recovery window can't clobber the file first. */
|
|
46
|
+
recovered = false;
|
|
47
|
+
constructor(callbacks) {
|
|
48
|
+
this.callbacks = callbacks;
|
|
49
|
+
this.pipeline = new RunnerStdoutPipeline({
|
|
50
|
+
backend: this.backend,
|
|
51
|
+
callbacks: this.callbacks,
|
|
52
|
+
bus: this.bus,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
start() {
|
|
56
|
+
if (this.started)
|
|
57
|
+
return;
|
|
58
|
+
this.started = true;
|
|
59
|
+
setTimeout(() => { void this.recover(); }, RECOVER_DELAY_MS);
|
|
60
|
+
log.log('🛡️ Codex app-server runner started (streaming, restart-surviving)');
|
|
61
|
+
}
|
|
62
|
+
ensureProcess(spawnCwd) {
|
|
63
|
+
if (this.process && this.process.isAlive())
|
|
64
|
+
return this.process;
|
|
65
|
+
this.process = new CodexAppServerProcess({
|
|
66
|
+
executablePath: this.backend.getExecutablePath(),
|
|
67
|
+
cwd: spawnCwd || os.homedir(),
|
|
68
|
+
extraEnv: this.backend.getExtraEnv(),
|
|
69
|
+
onNotification: (method, params) => this.routeNotification(method, params),
|
|
70
|
+
onExit: () => this.handleProcessExit(),
|
|
71
|
+
});
|
|
72
|
+
return this.process;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* On boot, rejoin the surviving daemon and re-subscribe each persisted agent's
|
|
76
|
+
* thread so an in-flight turn keeps streaming and finalizes correctly.
|
|
77
|
+
*/
|
|
78
|
+
async recover() {
|
|
79
|
+
const persisted = loadAppServerAgents();
|
|
80
|
+
this.recovered = true;
|
|
81
|
+
if (persisted.length === 0) {
|
|
82
|
+
this.persistAgents();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const spawnCwd = persisted[0].workingDir || os.homedir();
|
|
86
|
+
const proc = this.ensureProcess(spawnCwd);
|
|
87
|
+
try {
|
|
88
|
+
await proc.ensureStarted();
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
92
|
+
log.warn(`Recovery: could not reach app-server (${msg}); agents will lazy-resume on next message`);
|
|
93
|
+
this.finalizeStale(persisted);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!proc.didReconnect()) {
|
|
97
|
+
// A fresh daemon was spawned — the old in-flight turns are gone with the
|
|
98
|
+
// old daemon. sessionId is persisted on each agent, so the next message
|
|
99
|
+
// resumes the conversation; just finalize any that were mid-turn.
|
|
100
|
+
log.log('Recovery: fresh daemon (old turns gone); finalizing mid-turn agents');
|
|
101
|
+
this.finalizeStale(persisted);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
log.log(`🔗 Recovery: rejoined daemon; re-subscribing ${persisted.length} thread(s)`);
|
|
105
|
+
for (const p of persisted) {
|
|
106
|
+
await this.resubscribe(proc, p);
|
|
107
|
+
}
|
|
108
|
+
this.persistAgents();
|
|
109
|
+
}
|
|
110
|
+
finalizeStale(persisted) {
|
|
111
|
+
for (const p of persisted) {
|
|
112
|
+
if (p.agentStatus === 'working' || p.turnState === 'processing') {
|
|
113
|
+
this.callbacks.onComplete(p.agentId, true);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
saveAppServerAgents([]);
|
|
117
|
+
}
|
|
118
|
+
async resubscribe(proc, p) {
|
|
119
|
+
const state = {
|
|
120
|
+
threadId: p.threadId,
|
|
121
|
+
sessionId: p.threadId,
|
|
122
|
+
adapter: new CodexAppServerEventAdapter(),
|
|
123
|
+
turnState: p.turnState,
|
|
124
|
+
startTime: Date.now(),
|
|
125
|
+
lastActivityTime: Date.now(),
|
|
126
|
+
lastRequest: p.lastRequest,
|
|
127
|
+
queue: [],
|
|
128
|
+
};
|
|
129
|
+
this.agents.set(p.agentId, state);
|
|
130
|
+
this.threadToAgent.set(p.threadId, p.agentId);
|
|
131
|
+
if (p.agentStatus === 'working' && agentService.getAgent(p.agentId)) {
|
|
132
|
+
agentService.updateAgent(p.agentId, { status: 'working' });
|
|
133
|
+
}
|
|
134
|
+
// thread/resume re-attaches this connection to the thread's live stream (the
|
|
135
|
+
// server replays an in-flight turn's remaining deltas). thread/read tells us
|
|
136
|
+
// whether the turn already ended while we were down.
|
|
137
|
+
proc.request('thread/resume', { threadId: p.threadId }).catch(() => { });
|
|
138
|
+
let read = null;
|
|
139
|
+
try {
|
|
140
|
+
read = await proc.request('thread/read', { threadId: p.threadId, includeTurns: true });
|
|
141
|
+
}
|
|
142
|
+
catch { /* ignore */ }
|
|
143
|
+
const turns = read?.thread?.turns
|
|
144
|
+
?? read?.turns;
|
|
145
|
+
const latest = Array.isArray(turns) && turns.length > 0 ? turns[turns.length - 1] : undefined;
|
|
146
|
+
if (latest && latest.status && latest.status !== 'inProgress') {
|
|
147
|
+
// The turn finished during the downtime — we won't get a fresh
|
|
148
|
+
// turn/completed, so finalize now.
|
|
149
|
+
state.turnState = 'waiting_for_input';
|
|
150
|
+
this.callbacks.onComplete(p.agentId, latest.status !== 'failed');
|
|
151
|
+
}
|
|
152
|
+
// Otherwise it's still inProgress: thread/resume subscribed us, and the
|
|
153
|
+
// replayed deltas + turn/completed will finalize it via applyEvent().
|
|
154
|
+
}
|
|
155
|
+
handleProcessExit() {
|
|
156
|
+
// The WebSocket to the daemon dropped. Finalize live agents so none stays
|
|
157
|
+
// stuck 'working'; the daemon itself likely survives, so the next message
|
|
158
|
+
// reconnects and resumes.
|
|
159
|
+
const agentIds = [...this.agents.keys()];
|
|
160
|
+
log.warn(`app-server connection lost — finalizing ${agentIds.length} agent(s)`);
|
|
161
|
+
this.agents.clear();
|
|
162
|
+
this.threadToAgent.clear();
|
|
163
|
+
this.process = null;
|
|
164
|
+
for (const agentId of agentIds)
|
|
165
|
+
this.callbacks.onComplete(agentId, false);
|
|
166
|
+
}
|
|
167
|
+
routeNotification(method, params) {
|
|
168
|
+
const threadId = typeof params.threadId === 'string' ? params.threadId : undefined;
|
|
169
|
+
if (!threadId)
|
|
170
|
+
return;
|
|
171
|
+
const agentId = this.threadToAgent.get(threadId);
|
|
172
|
+
if (!agentId)
|
|
173
|
+
return;
|
|
174
|
+
const state = this.agents.get(agentId);
|
|
175
|
+
if (!state)
|
|
176
|
+
return;
|
|
177
|
+
state.lastActivityTime = Date.now();
|
|
178
|
+
this.fireActivity(agentId);
|
|
179
|
+
for (const event of state.adapter.handle(method, params)) {
|
|
180
|
+
this.applyEvent(agentId, state, event);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
applyEvent(agentId, state, event) {
|
|
184
|
+
if (event.type === 'text' || event.type === 'tool_start' || event.type === 'thinking' || event.type === 'init') {
|
|
185
|
+
state.turnState = 'processing';
|
|
186
|
+
}
|
|
187
|
+
this.pipeline.emitStandardEvent(agentId, event);
|
|
188
|
+
if (event.type === 'step_complete') {
|
|
189
|
+
state.turnState = 'waiting_for_input';
|
|
190
|
+
this.persistAgents();
|
|
191
|
+
// The daemon stays alive across turns, so there's no process 'close' to
|
|
192
|
+
// flip the agent idle. runtime-events skips the idle transition for codex
|
|
193
|
+
// on step_complete (it expects it on process exit), so drive it here.
|
|
194
|
+
this.callbacks.onComplete(agentId, true);
|
|
195
|
+
this.drainQueue(agentId, state);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async run(request) {
|
|
199
|
+
await withAgentContext(request.agentId, () => this.runImpl(request));
|
|
200
|
+
}
|
|
201
|
+
async runImpl(request) {
|
|
202
|
+
const { agentId, workingDir } = request;
|
|
203
|
+
const proc = this.ensureProcess(workingDir);
|
|
204
|
+
try {
|
|
205
|
+
await proc.ensureStarted();
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
209
|
+
log.error(`Failed to start app-server for ${agentId.slice(0, 8)}: ${msg}`);
|
|
210
|
+
this.callbacks.onError(agentId, `Codex app-server failed to start: ${msg}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const wantNew = request.forceNewSession || !request.sessionId;
|
|
214
|
+
let state = this.agents.get(agentId);
|
|
215
|
+
if (state && wantNew) {
|
|
216
|
+
this.threadToAgent.delete(state.threadId);
|
|
217
|
+
this.agents.delete(agentId);
|
|
218
|
+
state = undefined;
|
|
219
|
+
}
|
|
220
|
+
if (!state) {
|
|
221
|
+
const threadId = await this.openThread(agentId, request, proc, wantNew);
|
|
222
|
+
if (!threadId)
|
|
223
|
+
return;
|
|
224
|
+
state = {
|
|
225
|
+
threadId,
|
|
226
|
+
sessionId: threadId,
|
|
227
|
+
adapter: new CodexAppServerEventAdapter(),
|
|
228
|
+
turnState: 'processing',
|
|
229
|
+
startTime: Date.now(),
|
|
230
|
+
lastActivityTime: Date.now(),
|
|
231
|
+
lastRequest: request,
|
|
232
|
+
queue: [],
|
|
233
|
+
};
|
|
234
|
+
this.agents.set(agentId, state);
|
|
235
|
+
this.threadToAgent.set(threadId, agentId);
|
|
236
|
+
agentService.updateAgent(agentId, { sessionId: threadId });
|
|
237
|
+
this.callbacks.onSessionId(agentId, threadId);
|
|
238
|
+
this.pipeline.emitStandardEvent(agentId, {
|
|
239
|
+
type: 'init',
|
|
240
|
+
sessionId: threadId,
|
|
241
|
+
model: this.threadModel(request) || 'codex',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
state.lastRequest = request;
|
|
246
|
+
}
|
|
247
|
+
await this.startTurn(agentId, state, request.prompt, proc);
|
|
248
|
+
this.persistAgents();
|
|
249
|
+
}
|
|
250
|
+
async openThread(agentId, request, proc, wantNew) {
|
|
251
|
+
const baseParams = {
|
|
252
|
+
cwd: request.workingDir,
|
|
253
|
+
approvalPolicy: 'never',
|
|
254
|
+
sandbox: 'danger-full-access',
|
|
255
|
+
};
|
|
256
|
+
const model = this.threadModel(request);
|
|
257
|
+
if (model)
|
|
258
|
+
baseParams.model = model;
|
|
259
|
+
const effort = request.codexConfig?.reasoningEffort;
|
|
260
|
+
if (effort)
|
|
261
|
+
baseParams.config = { model_reasoning_effort: effort };
|
|
262
|
+
try {
|
|
263
|
+
if (!wantNew && request.sessionId) {
|
|
264
|
+
const result = (await proc.request('thread/resume', {
|
|
265
|
+
...baseParams,
|
|
266
|
+
threadId: request.sessionId,
|
|
267
|
+
}));
|
|
268
|
+
const id = result?.thread?.id || request.sessionId;
|
|
269
|
+
log.log(`🔄 Resumed thread ${id.slice(0, 8)} for agent ${agentId.slice(0, 8)}`);
|
|
270
|
+
return id;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
275
|
+
log.warn(`thread/resume failed for ${agentId.slice(0, 8)} (${msg}) — starting a fresh thread`);
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const result = (await proc.request('thread/start', baseParams));
|
|
279
|
+
const id = result?.thread?.id;
|
|
280
|
+
if (!id)
|
|
281
|
+
throw new Error('thread/start returned no thread id');
|
|
282
|
+
log.log(`🆕 Started thread ${id.slice(0, 8)} for agent ${agentId.slice(0, 8)}`);
|
|
283
|
+
return id;
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
287
|
+
log.error(`thread/start failed for ${agentId.slice(0, 8)}: ${msg}`);
|
|
288
|
+
this.callbacks.onError(agentId, `Codex app-server thread failed: ${msg}`);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
threadModel(request) {
|
|
293
|
+
const model = request.model;
|
|
294
|
+
if (!model)
|
|
295
|
+
return undefined;
|
|
296
|
+
if (model === 'codex' || model === 'sonnet' || model === 'opus' || model === 'haiku' || model.startsWith('claude-')) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
return model;
|
|
300
|
+
}
|
|
301
|
+
async startTurn(agentId, state, prompt, proc) {
|
|
302
|
+
const config = {
|
|
303
|
+
agentId,
|
|
304
|
+
sessionId: state.threadId,
|
|
305
|
+
model: state.lastRequest.model,
|
|
306
|
+
workingDir: state.lastRequest.workingDir,
|
|
307
|
+
systemPrompt: state.lastRequest.systemPrompt,
|
|
308
|
+
customAgent: state.lastRequest.customAgent,
|
|
309
|
+
codexConfig: state.lastRequest.codexConfig,
|
|
310
|
+
prompt,
|
|
311
|
+
};
|
|
312
|
+
const promptText = buildCodexPrompt(config);
|
|
313
|
+
state.turnState = 'processing';
|
|
314
|
+
state.lastActivityTime = Date.now();
|
|
315
|
+
try {
|
|
316
|
+
await proc.request('turn/start', {
|
|
317
|
+
threadId: state.threadId,
|
|
318
|
+
input: [{ type: 'text', text: promptText }],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
323
|
+
log.error(`turn/start failed for ${agentId.slice(0, 8)}: ${msg}`);
|
|
324
|
+
this.callbacks.onError(agentId, `Codex turn failed: ${msg}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
drainQueue(agentId, state) {
|
|
328
|
+
if (state.queue.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
const next = state.queue.shift();
|
|
331
|
+
const proc = this.process;
|
|
332
|
+
if (!proc || !proc.isAlive()) {
|
|
333
|
+
state.queue.unshift(next);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
log.log(`📤 Delivering queued follow-up to ${agentId.slice(0, 8)} (${state.queue.length} remaining)`);
|
|
337
|
+
agentService.updateAgent(agentId, { status: 'working', currentTask: next.substring(0, 100) });
|
|
338
|
+
void this.startTurn(agentId, state, next, proc);
|
|
339
|
+
}
|
|
340
|
+
sendMessage(agentId, message) {
|
|
341
|
+
return withAgentContext(agentId, () => {
|
|
342
|
+
const state = this.agents.get(agentId);
|
|
343
|
+
if (!state) {
|
|
344
|
+
log.warn(`sendMessage: no live thread for ${agentId.slice(0, 8)}`);
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
const proc = this.process;
|
|
348
|
+
if (!proc || !proc.isAlive())
|
|
349
|
+
return false;
|
|
350
|
+
if (state.turnState === 'processing') {
|
|
351
|
+
state.queue.push(message);
|
|
352
|
+
log.log(`📋 Queued mid-turn message for ${agentId.slice(0, 8)} (queue=${state.queue.length})`);
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
void this.startTurn(agentId, state, message, proc);
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
async stop(agentId, _clearQueue = true) {
|
|
360
|
+
await withAgentContext(agentId, async () => {
|
|
361
|
+
const state = this.agents.get(agentId);
|
|
362
|
+
if (!state)
|
|
363
|
+
return;
|
|
364
|
+
const proc = this.process;
|
|
365
|
+
if (proc && proc.isAlive() && state.turnState === 'processing') {
|
|
366
|
+
try {
|
|
367
|
+
await proc.request('turn/interrupt', { threadId: state.threadId });
|
|
368
|
+
}
|
|
369
|
+
catch { /* best-effort */ }
|
|
370
|
+
}
|
|
371
|
+
this.threadToAgent.delete(state.threadId);
|
|
372
|
+
this.agents.delete(agentId);
|
|
373
|
+
this.activityCallbacks.delete(agentId);
|
|
374
|
+
this.persistAgents();
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
async stopAll(killProcesses = true, _clearQueue) {
|
|
378
|
+
this.agents.clear();
|
|
379
|
+
this.threadToAgent.clear();
|
|
380
|
+
this.activityCallbacks.clear();
|
|
381
|
+
if (this.process) {
|
|
382
|
+
if (killProcesses) {
|
|
383
|
+
// Explicit full shutdown → kill the daemon and drop recovery state.
|
|
384
|
+
this.process.killDaemon();
|
|
385
|
+
saveAppServerAgents([]);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// Graceful restart → leave the daemon running so we rejoin it on boot.
|
|
389
|
+
this.persistAgents();
|
|
390
|
+
this.process.disconnect();
|
|
391
|
+
}
|
|
392
|
+
this.process = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
isRunning(agentId) {
|
|
396
|
+
return this.agents.has(agentId) && this.process !== null && this.process.isAlive();
|
|
397
|
+
}
|
|
398
|
+
supportsStdin() {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
closesStdinAfterPrompt() {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
getTurnState(agentId) {
|
|
405
|
+
return this.agents.get(agentId)?.turnState;
|
|
406
|
+
}
|
|
407
|
+
hasRecentActivity(agentId, withinMs) {
|
|
408
|
+
const state = this.agents.get(agentId);
|
|
409
|
+
if (!state)
|
|
410
|
+
return false;
|
|
411
|
+
const last = state.lastActivityTime || state.startTime;
|
|
412
|
+
return Date.now() - last < withinMs;
|
|
413
|
+
}
|
|
414
|
+
onNextActivity(agentId, callback) {
|
|
415
|
+
if (!this.activityCallbacks.has(agentId))
|
|
416
|
+
this.activityCallbacks.set(agentId, []);
|
|
417
|
+
this.activityCallbacks.get(agentId).push(callback);
|
|
418
|
+
}
|
|
419
|
+
fireActivity(agentId) {
|
|
420
|
+
const cbs = this.activityCallbacks.get(agentId);
|
|
421
|
+
if (!cbs || cbs.length === 0)
|
|
422
|
+
return;
|
|
423
|
+
this.activityCallbacks.delete(agentId);
|
|
424
|
+
for (const cb of cbs) {
|
|
425
|
+
try {
|
|
426
|
+
cb();
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
log.error(`Activity callback error for ${agentId}:`, err);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
persistAgents() {
|
|
434
|
+
if (!this.recovered)
|
|
435
|
+
return; // don't clobber the recovery file before reading it
|
|
436
|
+
const list = [];
|
|
437
|
+
for (const [agentId, state] of this.agents) {
|
|
438
|
+
const agent = agentService.getAgent(agentId);
|
|
439
|
+
list.push({
|
|
440
|
+
agentId,
|
|
441
|
+
threadId: state.threadId,
|
|
442
|
+
workingDir: state.lastRequest.workingDir,
|
|
443
|
+
turnState: state.turnState,
|
|
444
|
+
agentStatus: agent?.status,
|
|
445
|
+
lastRequest: state.lastRequest,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
saveAppServerAgents(list);
|
|
449
|
+
}
|
|
450
|
+
getActiveProcessesState() {
|
|
451
|
+
return [...this.agents.keys()].map((agentId) => ({ agentId, pid: undefined }));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
@@ -18,7 +18,7 @@ import { truncateOrEmpty } from '../utils/string.js';
|
|
|
18
18
|
import { buildCustomAgentConfig, expandFileMentions } from '../websocket/handlers/command-handler.js';
|
|
19
19
|
import { clearDelegation, getBossForSubordinate } from '../websocket/handlers/boss-response-handler.js';
|
|
20
20
|
import { OpencodeBackend } from '../opencode/backend.js';
|
|
21
|
-
import { getSystemPrompt, setSystemPrompt, clearSystemPrompt, isEchoPromptEnabled, setEchoPromptEnabled, getCodexBinaryPath, setCodexBinaryPath, isTmuxModeEnabled, setTmuxModeEnabled, isInteractiveModeEnabled, setInteractiveModeEnabled } from '../services/system-prompt-service.js';
|
|
21
|
+
import { getSystemPrompt, setSystemPrompt, clearSystemPrompt, isEchoPromptEnabled, setEchoPromptEnabled, getCodexBinaryPath, setCodexBinaryPath, isTmuxModeEnabled, setTmuxModeEnabled, isInteractiveModeEnabled, setInteractiveModeEnabled, isCodexAppServerModeEnabled, setCodexAppServerModeEnabled } from '../services/system-prompt-service.js';
|
|
22
22
|
import { markInstructionsDirtyForAll } from '../services/instruction-refresh.js';
|
|
23
23
|
import { startAgentTerminal, stopAgentTerminal } from '../services/agent-terminal-service.js';
|
|
24
24
|
import { buildClaudeUsageByAgentSummary, buildClaudeUsageByDaySummary, buildClaudeUsageSnapshot } from '../services/claude-usage-service.js';
|
|
@@ -1501,6 +1501,34 @@ router.post('/system-settings/interactive-mode', (req, res) => {
|
|
|
1501
1501
|
res.status(500).json({ error: err.message });
|
|
1502
1502
|
}
|
|
1503
1503
|
});
|
|
1504
|
+
// GET /api/system-settings/codex-app-server-mode - Get experimental Codex app-server (streaming) mode setting
|
|
1505
|
+
router.get('/system-settings/codex-app-server-mode', (_req, res) => {
|
|
1506
|
+
try {
|
|
1507
|
+
const enabled = isCodexAppServerModeEnabled();
|
|
1508
|
+
res.json({ enabled });
|
|
1509
|
+
}
|
|
1510
|
+
catch (err) {
|
|
1511
|
+
log.error(' Failed to get codex app-server mode setting:', err);
|
|
1512
|
+
res.status(500).json({ error: err.message });
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
// POST /api/system-settings/codex-app-server-mode - Update experimental Codex app-server (streaming) mode setting
|
|
1516
|
+
router.post('/system-settings/codex-app-server-mode', (req, res) => {
|
|
1517
|
+
try {
|
|
1518
|
+
const { enabled } = req.body;
|
|
1519
|
+
if (typeof enabled !== 'boolean') {
|
|
1520
|
+
res.status(400).json({ error: 'enabled must be a boolean' });
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
setCodexAppServerModeEnabled(enabled);
|
|
1524
|
+
log.log(` Codex app-server mode setting updated: enabled=${enabled}`);
|
|
1525
|
+
res.json({ success: true, enabled });
|
|
1526
|
+
}
|
|
1527
|
+
catch (err) {
|
|
1528
|
+
log.error(' Failed to set codex app-server mode setting:', err);
|
|
1529
|
+
res.status(500).json({ error: err.message });
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1504
1532
|
// GET /api/system-settings/backup - Get hourly backup scheduler status
|
|
1505
1533
|
router.get('/system-settings/backup', (_req, res) => {
|
|
1506
1534
|
try {
|