pikiloom 0.4.26 → 0.4.28
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/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
- package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
- package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/acp-client.js +0 -59
- package/dist/agent/artifacts.js +1 -38
- package/dist/agent/auto-update.js +2 -78
- package/dist/agent/await-resume.js +0 -20
- package/dist/agent/cli/auth.js +3 -51
- package/dist/agent/cli/catalog.js +0 -3
- package/dist/agent/cli/detector.js +1 -19
- package/dist/agent/cli/index.js +0 -3
- package/dist/agent/cli/registry.js +0 -25
- package/dist/agent/driver.js +0 -14
- package/dist/agent/drivers/claude-tui.js +2 -769
- package/dist/agent/drivers/claude.js +12 -631
- package/dist/agent/drivers/codex.js +11 -162
- package/dist/agent/drivers/gemini.js +6 -114
- package/dist/agent/drivers/hermes.js +0 -128
- package/dist/agent/goal.js +0 -45
- package/dist/agent/handover.js +1 -46
- package/dist/agent/images.js +7 -133
- package/dist/agent/index.js +1 -29
- package/dist/agent/mcp/bridge.js +1 -197
- package/dist/agent/mcp/extensions.js +5 -84
- package/dist/agent/mcp/oauth.js +2 -69
- package/dist/agent/mcp/registry.js +2 -33
- package/dist/agent/mcp/session-server.js +2 -45
- package/dist/agent/mcp/tools/ask-user.js +0 -6
- package/dist/agent/mcp/tools/await-resume.js +0 -15
- package/dist/agent/mcp/tools/goal.js +0 -25
- package/dist/agent/mcp/tools/types.js +0 -5
- package/dist/agent/mcp/tools/workspace.js +0 -23
- package/dist/agent/npm.js +0 -14
- package/dist/agent/session.js +86 -196
- package/dist/agent/skill-installer.js +3 -70
- package/dist/agent/skills.js +0 -43
- package/dist/agent/stream.js +5 -148
- package/dist/agent/types.js +0 -12
- package/dist/agent/utils.js +2 -120
- package/dist/bot/bot.js +3 -468
- package/dist/bot/command-ui.js +0 -45
- package/dist/bot/commands.js +0 -51
- package/dist/bot/headless-bot.js +0 -21
- package/dist/bot/host.js +1 -5
- package/dist/bot/human-loop.js +0 -8
- package/dist/bot/menu.js +0 -3
- package/dist/bot/orchestration.js +0 -3
- package/dist/bot/render-shared.js +0 -63
- package/dist/bot/session-hub.js +14 -92
- package/dist/bot/session-status.js +1 -5
- package/dist/bot/streaming.js +0 -53
- package/dist/browser-profile.js +0 -51
- package/dist/browser-supervisor.js +0 -75
- package/dist/catalog/cli-tools.js +0 -53
- package/dist/catalog/index.js +0 -18
- package/dist/catalog/local-models.js +0 -18
- package/dist/catalog/mcp-servers.js +0 -35
- package/dist/catalog/skill-repos.js +0 -32
- package/dist/channels/base.js +0 -19
- package/dist/channels/dingtalk/bot.js +0 -3
- package/dist/channels/dingtalk/channel.js +0 -33
- package/dist/channels/discord/bot.js +0 -7
- package/dist/channels/discord/channel.js +0 -22
- package/dist/channels/feishu/bot.js +0 -71
- package/dist/channels/feishu/channel.js +0 -78
- package/dist/channels/feishu/markdown.js +0 -5
- package/dist/channels/feishu/render.js +0 -45
- package/dist/channels/health.js +0 -46
- package/dist/channels/slack/bot.js +0 -7
- package/dist/channels/slack/channel.js +1 -27
- package/dist/channels/states.js +0 -3
- package/dist/channels/telegram/bot.js +0 -60
- package/dist/channels/telegram/channel.js +2 -99
- package/dist/channels/telegram/directory.js +0 -3
- package/dist/channels/telegram/live-preview.js +0 -8
- package/dist/channels/telegram/render.js +0 -30
- package/dist/channels/wecom/bot.js +0 -3
- package/dist/channels/wecom/channel.js +0 -58
- package/dist/channels/weixin/api.js +0 -9
- package/dist/channels/weixin/bot.js +0 -88
- package/dist/channels/weixin/channel.js +0 -6
- package/dist/cli/autostart.js +1 -44
- package/dist/cli/channel-supervisor.js +1 -59
- package/dist/cli/channels.js +0 -6
- package/dist/cli/main.js +1 -145
- package/dist/cli/onboarding.js +0 -6
- package/dist/cli/run.js +0 -11
- package/dist/cli/setup-wizard.js +0 -3
- package/dist/core/config/runtime-config.js +0 -38
- package/dist/core/config/user-config.js +0 -63
- package/dist/core/config/validation.js +0 -9
- package/dist/core/constants.js +0 -268
- package/dist/core/git.js +0 -31
- package/dist/core/legacy-compat.js +1 -28
- package/dist/core/logging.js +0 -3
- package/dist/core/platform.js +0 -28
- package/dist/core/process-control.js +0 -8
- package/dist/core/secrets/index.js +0 -17
- package/dist/core/secrets/inline-seal.js +0 -7
- package/dist/core/secrets/ref.js +0 -19
- package/dist/core/secrets/resolver.js +0 -8
- package/dist/core/secrets/store.js +1 -12
- package/dist/core/utils.js +1 -37
- package/dist/core/version.js +0 -3
- package/dist/dashboard/platform.js +1 -35
- package/dist/dashboard/routes/agents.js +2 -71
- package/dist/dashboard/routes/cli.js +0 -27
- package/dist/dashboard/routes/config.js +0 -54
- package/dist/dashboard/routes/extensions.js +0 -132
- package/dist/dashboard/routes/local-models.js +0 -68
- package/dist/dashboard/routes/models.js +0 -46
- package/dist/dashboard/routes/sessions.js +0 -100
- package/dist/dashboard/runtime.js +1 -58
- package/dist/dashboard/server.js +0 -49
- package/dist/dashboard/session-control.js +2 -69
- package/dist/model/catalog.js +1 -17
- package/dist/model/index.js +0 -15
- package/dist/model/injector.js +1 -181
- package/dist/model/provider-models.js +1 -48
- package/dist/model/responses-bridge.js +3 -45
- package/dist/model/store.js +0 -17
- package/dist/model/types.js +0 -12
- package/dist/model/validation.js +1 -18
- package/dist/pikichannel/adapter-pikiloom.js +0 -31
- package/dist/pikichannel/code.js +0 -12
- package/dist/pikichannel/codec.js +0 -14
- package/dist/pikichannel/host.js +4 -33
- package/dist/pikichannel/protocol.js +0 -52
- package/dist/pikichannel/rendezvous-broker.js +5 -26
- package/dist/pikichannel/rendezvous-host.js +3 -16
- package/dist/pikichannel/server.js +6 -55
- package/dist/pikichannel/transport.js +0 -25
- package/dist/pikichannel/transports/webrtc-host.js +0 -18
- package/dist/pikichannel/transports/webrtc-shared.js +4 -34
- package/dist/pikichannel/transports/websocket-host.js +1 -14
- package/dist/pikichannel/turn.js +2 -69
- package/dist/pikichannel/web/sdk.js +14 -71
- package/package.json +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
- package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
- package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
- package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
- package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
- package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
package/dist/bot/bot.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Bot base class: chat state, session lifecycle, task queue, streaming bridge.
|
|
3
|
-
*
|
|
4
|
-
* Channel-agnostic. Subclassed per IM channel (see channels/telegram/bot.ts, etc.).
|
|
5
|
-
*/
|
|
6
1
|
import os from 'node:os';
|
|
7
2
|
import path from 'node:path';
|
|
8
3
|
import { execSync, spawn } from 'node:child_process';
|
|
@@ -29,9 +24,6 @@ import { BOT_TIMEOUTS } from '../core/constants.js';
|
|
|
29
24
|
export const DEFAULT_RUN_TIMEOUT_S = BOT_TIMEOUTS.defaultRunTimeoutS;
|
|
30
25
|
const MACOS_USER_ACTIVITY_PULSE_INTERVAL_MS = BOT_TIMEOUTS.macosUserActivityPulseInterval;
|
|
31
26
|
const MACOS_USER_ACTIVITY_PULSE_TIMEOUT_S = BOT_TIMEOUTS.macosUserActivityPulseTimeoutS;
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Helpers
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
27
|
export function normalizeAgent(raw) {
|
|
36
28
|
const v = raw.trim().toLowerCase();
|
|
37
29
|
if (!hasDriver(v))
|
|
@@ -46,9 +38,6 @@ export function thinkLabel(agent) {
|
|
|
46
38
|
return 'Thinking';
|
|
47
39
|
}
|
|
48
40
|
}
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Prompt assembly helpers
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
41
|
function appendExtraPrompt(base, extra) {
|
|
53
42
|
const lhs = String(base || '').trim();
|
|
54
43
|
const rhs = String(extra || '').trim();
|
|
@@ -58,10 +47,6 @@ function appendExtraPrompt(base, extra) {
|
|
|
58
47
|
return lhs;
|
|
59
48
|
return `${lhs}\n\n${rhs}`;
|
|
60
49
|
}
|
|
61
|
-
// NOTE: the `[Artifact Return]` header is a load-bearing marker — both
|
|
62
|
-
// `stripInjectedPrompts` (bot/streaming.ts) and Gemini's
|
|
63
|
-
// `GEMINI_SYSTEM_BLOCK_SENTINELS` key on it to strip this injected block from
|
|
64
|
-
// displayed user messages. Keep the token in sync if you ever change it.
|
|
65
50
|
function buildMcpDeliveryPrompt() {
|
|
66
51
|
return [
|
|
67
52
|
'[Artifact Return]',
|
|
@@ -69,13 +54,6 @@ function buildMcpDeliveryPrompt() {
|
|
|
69
54
|
].join('\n');
|
|
70
55
|
}
|
|
71
56
|
function buildClaudeAskUserPrompt() {
|
|
72
|
-
// Claude is heavily trained on its built-in `AskUserQuestion` tool, so just
|
|
73
|
-
// registering `mcp__pikiloom__im_ask_user` alongside it isn't enough — the
|
|
74
|
-
// model still picks the native one, the CLI rejects it in -p mode with
|
|
75
|
-
// `is_error: true content: "Answer questions?"`, and the turn dies without
|
|
76
|
-
// ever firing the human-loop. This directive redirects calls *if* the model
|
|
77
|
-
// chooses to ask. It deliberately does not nudge the default ask-less
|
|
78
|
-
// behaviour — only the routing.
|
|
79
57
|
return [
|
|
80
58
|
'[Asking the user]',
|
|
81
59
|
'The built-in `AskUserQuestion` tool is disabled here and will fail. If you would otherwise call it, call `mcp__pikiloom__im_ask_user` instead — same intent (a question plus optional choices), it blocks until the user replies via the IM/dashboard channel. Default behaviour is unchanged: infer obvious decisions yourself and only ask when you genuinely cannot proceed.',
|
|
@@ -99,11 +77,6 @@ function buildBrowserAutomationPrompt(browserEnabled) {
|
|
|
99
77
|
].join('\n');
|
|
100
78
|
}
|
|
101
79
|
function buildWorkflowOptInPrompt() {
|
|
102
|
-
// Standing opt-in injected only when the user explicitly enabled workflow
|
|
103
|
-
// orchestration for this agent. The Workflow tool is left enabled (not
|
|
104
|
-
// disallowed) in this mode; this directive tells the model it may reach for
|
|
105
|
-
// it proactively on genuinely large work, while preserving the default
|
|
106
|
-
// single-agent behaviour for everything else (no baseline regression).
|
|
107
80
|
return [
|
|
108
81
|
'[Multi-agent Workflow]',
|
|
109
82
|
'Workflow orchestration is enabled for this session. For substantial multi-step work — broad research, large refactors or audits, fan-out reviews across many files — you may proactively author and run a Workflow to decompose and parallelise it.',
|
|
@@ -136,7 +109,6 @@ function normalizeFromClaudeNative(goal) {
|
|
|
136
109
|
return {
|
|
137
110
|
source: 'claude',
|
|
138
111
|
objective: goal.condition,
|
|
139
|
-
// Native /goal exposes no pause/budget — it's either active or absent.
|
|
140
112
|
status: 'active',
|
|
141
113
|
tokenBudget: null,
|
|
142
114
|
tokensUsed: 0,
|
|
@@ -144,17 +116,12 @@ function normalizeFromClaudeNative(goal) {
|
|
|
144
116
|
continuationCount: null,
|
|
145
117
|
};
|
|
146
118
|
}
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
// Bot
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
119
|
export class Bot {
|
|
151
120
|
workdir;
|
|
152
121
|
defaultAgent;
|
|
153
122
|
runTimeout;
|
|
154
123
|
allowedChatIds;
|
|
155
|
-
// Per-agent config — keyed by agent id
|
|
156
124
|
agentConfigs = {};
|
|
157
|
-
// Convenience accessors (backward-compat)
|
|
158
125
|
get codexModel() { return this.agentConfigs.codex?.model || ''; }
|
|
159
126
|
set codexModel(v) { this.agentConfigs.codex.model = v; }
|
|
160
127
|
get codexReasoningEffort() { return this.agentConfigs.codex?.reasoningEffort || 'xhigh'; }
|
|
@@ -176,21 +143,10 @@ export class Bot {
|
|
|
176
143
|
startedAt = Date.now();
|
|
177
144
|
connected = false;
|
|
178
145
|
stats = { totalTurns: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCachedTokens: 0 };
|
|
179
|
-
/* ── Dashboard stream state (polling-friendly snapshots) ── */
|
|
180
146
|
streamSnapshots = new Map();
|
|
181
147
|
snapshotCleanupTimers = new Map();
|
|
182
|
-
/** Maps promoted session keys (old → new) so poll endpoints can resolve pending IDs. */
|
|
183
148
|
promotedSessionKeys = new Map();
|
|
184
|
-
/** Reverse map (new → old[]) so pushSnapshotToSSE can broadcast on promoted-from aliases. */
|
|
185
149
|
promotedFromAliases = new Map();
|
|
186
|
-
/**
|
|
187
|
-
* Walk the promotion chain so callers passing a stale (pending or
|
|
188
|
-
* pre-rotation) key always resolve to the current canonical key for the same
|
|
189
|
-
* logical session. Multi-hop chains (pending → id_a → id_b after Claude
|
|
190
|
-
* `--resume` rotates twice) are followed end-to-end. Used by every
|
|
191
|
-
* sessionStates / streamSnapshots lookup so the rest of the codebase never
|
|
192
|
-
* has to special-case promotion.
|
|
193
|
-
*/
|
|
194
150
|
resolveSessionKey(sessionKey) {
|
|
195
151
|
let key = sessionKey;
|
|
196
152
|
const seen = new Set();
|
|
@@ -203,13 +159,6 @@ export class Bot {
|
|
|
203
159
|
}
|
|
204
160
|
return key;
|
|
205
161
|
}
|
|
206
|
-
/**
|
|
207
|
-
* Drop all promotion bookkeeping that pointed at a now-retired canonical key.
|
|
208
|
-
* `promotedFromAliases.get(key)` is exactly the set of stale keys whose forward
|
|
209
|
-
* `promotedSessionKeys` entries resolve to `key`, so clearing them here keeps
|
|
210
|
-
* that map from growing for the whole process lifetime (otherwise one entry
|
|
211
|
-
* leaks per new session + per Claude `--resume` rotation, never reclaimed).
|
|
212
|
-
*/
|
|
213
162
|
forgetPromotion(canonicalKey) {
|
|
214
163
|
const aliases = this.promotedFromAliases.get(canonicalKey);
|
|
215
164
|
if (aliases)
|
|
@@ -217,32 +166,18 @@ export class Bot {
|
|
|
217
166
|
this.promotedSessionKeys.delete(alias);
|
|
218
167
|
this.promotedFromAliases.delete(canonicalKey);
|
|
219
168
|
}
|
|
220
|
-
/** Get the current streaming snapshot for a session (used by polling endpoint).
|
|
221
|
-
* Follows the promotion chain so a pending or pre-rotation key still resolves. */
|
|
222
169
|
getStreamSnapshot(sessionKey) {
|
|
223
170
|
const snap = this.streamSnapshots.get(this.resolveSessionKey(sessionKey));
|
|
224
171
|
return snap ? this.enrichSnapshot(snap) : null;
|
|
225
172
|
}
|
|
226
|
-
/**
|
|
227
|
-
* Attach per-queued-task prompts and refresh interaction `currentIndex` from
|
|
228
|
-
* live prompt state — both are derived data the cached snapshot can't carry
|
|
229
|
-
* on its own (queued prompts come from RunningTask records; currentIndex
|
|
230
|
-
* advances asynchronously after select/skip/text without re-emitting the
|
|
231
|
-
* interaction event).
|
|
232
|
-
*/
|
|
233
173
|
enrichSnapshot(snap) {
|
|
234
174
|
let next = snap;
|
|
235
|
-
// Attach the running turn's prompt so a watching terminal can render the
|
|
236
|
-
// user message for a follow-up it didn't originate (no local optimistic
|
|
237
|
-
// bubble). The RunningTask record is the source of truth while it's live.
|
|
238
175
|
const runningPrompt = next.taskId ? this.activeTasks.get(next.taskId)?.prompt : '';
|
|
239
176
|
if (runningPrompt)
|
|
240
177
|
next = { ...next, question: collapseSkillPrompt(runningPrompt) ?? runningPrompt };
|
|
241
178
|
if (next.queuedTaskIds?.length) {
|
|
242
179
|
const queuedTasks = next.queuedTaskIds.map(taskId => {
|
|
243
180
|
const raw = this.activeTasks.get(taskId)?.prompt || '';
|
|
244
|
-
// Show `/skillname` instead of the long expansion we synthesized for the
|
|
245
|
-
// agent — matches what the user actually typed in the queued row.
|
|
246
181
|
return { taskId, prompt: collapseSkillPrompt(raw) ?? raw };
|
|
247
182
|
});
|
|
248
183
|
next = { ...next, queuedTasks };
|
|
@@ -258,11 +193,9 @@ export class Bot {
|
|
|
258
193
|
}
|
|
259
194
|
return next;
|
|
260
195
|
}
|
|
261
|
-
/* ── Dashboard SSE push (injected by dashboard layer to avoid circular import) ── */
|
|
262
196
|
_onStreamSnapshot = null;
|
|
263
197
|
streamPushTimers = new Map();
|
|
264
198
|
streamPushPending = new Map();
|
|
265
|
-
/** Called by the dashboard layer to subscribe to stream snapshot changes. */
|
|
266
199
|
onStreamSnapshot(cb) {
|
|
267
200
|
this._onStreamSnapshot = cb;
|
|
268
201
|
}
|
|
@@ -274,8 +207,6 @@ export class Bot {
|
|
|
274
207
|
const emitAll = () => {
|
|
275
208
|
const enriched = snap ? this.enrichSnapshot(snap) : null;
|
|
276
209
|
cb(sessionKey, enriched);
|
|
277
|
-
// Also broadcast on promoted-from aliases so clients still listening
|
|
278
|
-
// on the old (pending) key receive updates after session promotion.
|
|
279
210
|
const aliases = this.promotedFromAliases.get(sessionKey);
|
|
280
211
|
if (aliases)
|
|
281
212
|
for (const alias of aliases)
|
|
@@ -291,7 +222,6 @@ export class Bot {
|
|
|
291
222
|
emitAll();
|
|
292
223
|
}
|
|
293
224
|
else {
|
|
294
|
-
// Coalesce: if a timer is pending, just mark dirty
|
|
295
225
|
this.streamPushPending.set(sessionKey, true);
|
|
296
226
|
if (this.streamPushTimers.has(sessionKey))
|
|
297
227
|
return;
|
|
@@ -304,9 +234,7 @@ export class Bot {
|
|
|
304
234
|
}, 80));
|
|
305
235
|
}
|
|
306
236
|
}
|
|
307
|
-
/** Emit a streaming event — updates the polling snapshot. */
|
|
308
237
|
emitStream(sessionKey, event) {
|
|
309
|
-
// Clear any pending cleanup timer
|
|
310
238
|
const pending = this.snapshotCleanupTimers.get(sessionKey);
|
|
311
239
|
if (pending) {
|
|
312
240
|
clearTimeout(pending);
|
|
@@ -317,7 +245,6 @@ export class Bot {
|
|
|
317
245
|
case 'queued': {
|
|
318
246
|
const existing = this.streamSnapshots.get(sessionKey);
|
|
319
247
|
if (existing && (existing.phase === 'streaming' || existing.phase === 'done')) {
|
|
320
|
-
// Don't overwrite active stream — append to the queued list (deduped).
|
|
321
248
|
const list = existing.queuedTaskIds ? [...existing.queuedTaskIds] : [];
|
|
322
249
|
if (existing.taskId !== event.taskId && !list.includes(event.taskId))
|
|
323
250
|
list.push(event.taskId);
|
|
@@ -325,7 +252,6 @@ export class Bot {
|
|
|
325
252
|
existing.updatedAt = now;
|
|
326
253
|
}
|
|
327
254
|
else if (existing && existing.phase === 'queued') {
|
|
328
|
-
// Already in queued phase with no active task — append additional queued IDs.
|
|
329
255
|
const list = existing.queuedTaskIds ? [...existing.queuedTaskIds] : [];
|
|
330
256
|
if (existing.taskId !== event.taskId && !list.includes(event.taskId))
|
|
331
257
|
list.push(event.taskId);
|
|
@@ -338,8 +264,6 @@ export class Bot {
|
|
|
338
264
|
break;
|
|
339
265
|
}
|
|
340
266
|
case 'start': {
|
|
341
|
-
// Preserve any tasks still queued behind the new active one. Drop the
|
|
342
|
-
// task that's now starting from that list since it has graduated.
|
|
343
267
|
const prev = this.streamSnapshots.get(sessionKey);
|
|
344
268
|
const remainingQueued = prev?.queuedTaskIds?.filter(id => id !== event.taskId);
|
|
345
269
|
this.streamSnapshots.set(sessionKey, {
|
|
@@ -392,9 +316,6 @@ export class Bot {
|
|
|
392
316
|
queuedTaskIds: prev?.queuedTaskIds,
|
|
393
317
|
updatedAt: now,
|
|
394
318
|
});
|
|
395
|
-
// Auto-clean 'done' snapshot after 30s so stale state doesn't linger.
|
|
396
|
-
// Extended from 10s to give clients time to pick up the final state
|
|
397
|
-
// after session promotion or WS reconnects.
|
|
398
319
|
this.snapshotCleanupTimers.set(sessionKey, setTimeout(() => {
|
|
399
320
|
this.streamSnapshots.delete(sessionKey);
|
|
400
321
|
this.snapshotCleanupTimers.delete(sessionKey);
|
|
@@ -407,14 +328,11 @@ export class Bot {
|
|
|
407
328
|
if (!snap)
|
|
408
329
|
break;
|
|
409
330
|
if (snap.queuedTaskIds?.includes(event.taskId)) {
|
|
410
|
-
// Cancelled one of the queued-behind tasks — keep the running/done
|
|
411
|
-
// snapshot, just remove this entry from the list.
|
|
412
331
|
const next = snap.queuedTaskIds.filter(id => id !== event.taskId);
|
|
413
332
|
snap.queuedTaskIds = next.length ? next : undefined;
|
|
414
333
|
snap.updatedAt = now;
|
|
415
334
|
}
|
|
416
335
|
else {
|
|
417
|
-
// Cancelled the currently displayed task — drop the whole snapshot.
|
|
418
336
|
this.streamSnapshots.delete(sessionKey);
|
|
419
337
|
this.forgetPromotion(sessionKey);
|
|
420
338
|
}
|
|
@@ -441,24 +359,11 @@ export class Bot {
|
|
|
441
359
|
break;
|
|
442
360
|
}
|
|
443
361
|
}
|
|
444
|
-
// Push to dashboard SSE — throttle text events, push everything else immediately
|
|
445
362
|
try {
|
|
446
363
|
this.pushSnapshotToSSE(sessionKey, event.type !== 'text');
|
|
447
364
|
}
|
|
448
|
-
catch {
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Stream-lifecycle helpers. The dashboard mirrors a running turn by reading
|
|
452
|
-
* `streamSnapshots`, which is built exclusively from `emitStream` calls.
|
|
453
|
-
* IM channels run `runStream` directly (not via `submitSessionTask`), so
|
|
454
|
-
* without these calls the dashboard never sees IM-initiated turns. Routing
|
|
455
|
-
* every IM handler through these helpers (and refactoring submitSessionTask
|
|
456
|
-
* to use them) keeps the two surfaces consistent: each side can observe
|
|
457
|
-
* whatever the other side started.
|
|
458
|
-
*
|
|
459
|
-
* Each helper resolves the live `task.sessionKey` so the event lands on the
|
|
460
|
-
* current snapshot after a pending→native session id promotion.
|
|
461
|
-
*/
|
|
365
|
+
catch { }
|
|
366
|
+
}
|
|
462
367
|
liveSessionKey(taskId, fallback) {
|
|
463
368
|
return this.activeTasks.get(taskId)?.sessionKey || fallback;
|
|
464
369
|
}
|
|
@@ -495,26 +400,12 @@ export class Bot {
|
|
|
495
400
|
emitStreamCancelled(taskId, fallbackKey) {
|
|
496
401
|
this.emitStream(this.liveSessionKey(taskId, fallbackKey), { type: 'cancelled', taskId });
|
|
497
402
|
}
|
|
498
|
-
/**
|
|
499
|
-
* Wrap a per-turn `im_send_file` callback so every artifact delivery is
|
|
500
|
-
* recorded to the session's durable manifest (agent/artifacts.ts) and
|
|
501
|
-
* mirrored live into the running turn's snapshot — on top of whatever
|
|
502
|
-
* terminal-specific push the caller supplied (`inner`, e.g. an IM chat
|
|
503
|
-
* upload). The returned callback is always safe to register, so the tool is
|
|
504
|
-
* available for dashboard / headless turns that have no `inner` (the
|
|
505
|
-
* dashboard serves the recorded copy over HTTP). Recording is best-effort and
|
|
506
|
-
* never propagates an error back into the delivery result.
|
|
507
|
-
*/
|
|
508
403
|
buildArtifactSendFile(agent, sessionKey, cs, inner) {
|
|
509
404
|
return async (filePath, sendOpts) => {
|
|
510
|
-
// Terminal-specific push first (IM chat). Dashboard / headless has no inner.
|
|
511
405
|
const result = inner ? await inner(filePath, sendOpts) : { ok: true };
|
|
512
406
|
if (!result.ok)
|
|
513
407
|
return result;
|
|
514
408
|
try {
|
|
515
|
-
// Resolve the freshest session id at delivery time — send_file fires
|
|
516
|
-
// late in a turn, after a pending→native promotion has normally already
|
|
517
|
-
// happened, so the manifest lands under the canonical id.
|
|
518
409
|
let sid = '';
|
|
519
410
|
if (sessionKey) {
|
|
520
411
|
const rt = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
@@ -563,7 +454,6 @@ export class Bot {
|
|
|
563
454
|
ensureGitignore(this.workdir);
|
|
564
455
|
initializeProjectSkills(this.workdir);
|
|
565
456
|
const config = getActiveUserConfig();
|
|
566
|
-
// Initialize per-agent configs
|
|
567
457
|
this.agentConfigs = {
|
|
568
458
|
codex: {
|
|
569
459
|
model: resolveAgentModel(config, 'codex'),
|
|
@@ -575,12 +465,7 @@ export class Bot {
|
|
|
575
465
|
model: resolveAgentModel(config, 'claude'),
|
|
576
466
|
reasoningEffort: resolveAgentEffort(config, 'claude') || 'high',
|
|
577
467
|
permissionMode: (process.env.CLAUDE_PERMISSION_MODE || 'bypassPermissions').trim(),
|
|
578
|
-
// Workflow orchestration is a per-session/per-turn choice (composer
|
|
579
|
-
// toggle / IM /mode), never a persisted default — always boot off.
|
|
580
468
|
workflowEnabled: false,
|
|
581
|
-
// Access mode (TUI subscription vs `claude -p` Agent SDK credits) IS a
|
|
582
|
-
// persisted preference — hydrate it from config so the boot value
|
|
583
|
-
// matches the dashboard toggle / env default.
|
|
584
469
|
accessMode: resolveClaudeAccessMode(config),
|
|
585
470
|
extraArgs: shellSplit(process.env.CLAUDE_EXTRA_ARGS || ''),
|
|
586
471
|
},
|
|
@@ -590,12 +475,6 @@ export class Bot {
|
|
|
590
475
|
sandbox: envBool('GEMINI_SANDBOX', false),
|
|
591
476
|
extraArgs: shellSplit(process.env.GEMINI_EXTRA_ARGS || ''),
|
|
592
477
|
},
|
|
593
|
-
// Hermes was missing from this map for a long time. Without an entry,
|
|
594
|
-
// `modelForAgent('hermes')` returned '' and `setModelForAgent('hermes',
|
|
595
|
-
// ...)` silently no-op'd because `if (config)` short-circuited — so any
|
|
596
|
-
// /models switch in IM looked successful in the log but never reached
|
|
597
|
-
// the hermes driver. Adding the entry lets the same machinery the other
|
|
598
|
-
// three agents already rely on apply to hermes too.
|
|
599
478
|
hermes: {
|
|
600
479
|
model: resolveAgentModel(config, 'hermes'),
|
|
601
480
|
reasoningEffort: resolveAgentEffort(config, 'hermes') || 'medium',
|
|
@@ -628,7 +507,6 @@ export class Bot {
|
|
|
628
507
|
}
|
|
629
508
|
return s;
|
|
630
509
|
}
|
|
631
|
-
/** Effective workdir for a chat — per-chat override or global fallback. */
|
|
632
510
|
chatWorkdir(chatId) {
|
|
633
511
|
return this.chats.get(chatId)?.workdir || this.workdir;
|
|
634
512
|
}
|
|
@@ -666,17 +544,10 @@ export class Bot {
|
|
|
666
544
|
upsertSessionRuntime(session) {
|
|
667
545
|
const workdir = path.resolve(session.workdir || this.workdir);
|
|
668
546
|
const requestedKey = this.sessionKey(session.agent, session.sessionId);
|
|
669
|
-
// Follow the promotion chain. Without this, an insertion that races a
|
|
670
|
-
// pending→native promotion would `new` a phantom runtime under the stale
|
|
671
|
-
// pending key and the queued message would land in a session the dashboard
|
|
672
|
-
// can never reach again.
|
|
673
547
|
const resolvedKey = this.resolveSessionKey(requestedKey);
|
|
674
548
|
const existing = this.sessionStates.get(resolvedKey);
|
|
675
549
|
if (existing) {
|
|
676
550
|
existing.workdir = workdir;
|
|
677
|
-
// Do NOT overwrite agent/sessionId/key here — the existing record IS the
|
|
678
|
-
// canonical identity post-promotion. Letting upserts re-stamp the old
|
|
679
|
-
// pending id back over the native id would unwind the promotion.
|
|
680
551
|
if (session.workspacePath !== undefined)
|
|
681
552
|
existing.workspacePath = session.workspacePath ?? null;
|
|
682
553
|
if (session.threadId !== undefined)
|
|
@@ -687,7 +558,6 @@ export class Bot {
|
|
|
687
558
|
existing.modelId = session.modelId ?? null;
|
|
688
559
|
if (session.thinkingEffort !== undefined)
|
|
689
560
|
existing.thinkingEffort = session.thinkingEffort ?? null;
|
|
690
|
-
// handoverFrom is one-shot: only set if not already set (the first staging wins).
|
|
691
561
|
if (session.handoverFrom !== undefined && !existing.handoverFrom) {
|
|
692
562
|
existing.handoverFrom = session.handoverFrom;
|
|
693
563
|
}
|
|
@@ -766,9 +636,6 @@ export class Bot {
|
|
|
766
636
|
this.applySessionSelection(cs, null);
|
|
767
637
|
return;
|
|
768
638
|
}
|
|
769
|
-
// Adopting an existing session is an explicit user pick — drop any
|
|
770
|
-
// queued handover from a prior agent toggle so we don't accidentally
|
|
771
|
-
// prepend the wrong context to the resumed session's next turn.
|
|
772
639
|
cs.pendingHandoverFrom = null;
|
|
773
640
|
this.applySessionSelection(cs, runtime);
|
|
774
641
|
}
|
|
@@ -839,11 +706,6 @@ export class Bot {
|
|
|
839
706
|
this.sessionChains.set(nextKey, mergedChain);
|
|
840
707
|
}
|
|
841
708
|
this.moveSessionStreamSnapshot(previousKey, nextKey);
|
|
842
|
-
// Track promotion so poll endpoints + insertions can resolve pending →
|
|
843
|
-
// native. When the chain hops more than once (Claude `--resume` rotating
|
|
844
|
-
// session ids back-to-back), pull ancestor aliases forward AND re-point
|
|
845
|
-
// them at the latest key so a single lookup is O(1) and every WS listener
|
|
846
|
-
// that subscribed to any earlier key still receives updates.
|
|
847
709
|
this.promotedSessionKeys.set(previousKey, nextKey);
|
|
848
710
|
const aliases = new Set(this.promotedFromAliases.get(nextKey) || []);
|
|
849
711
|
aliases.add(previousKey);
|
|
@@ -856,12 +718,9 @@ export class Bot {
|
|
|
856
718
|
this.promotedFromAliases.delete(previousKey);
|
|
857
719
|
}
|
|
858
720
|
this.promotedFromAliases.set(nextKey, [...aliases]);
|
|
859
|
-
// Update the promoted snapshot's sessionId to reflect the native ID
|
|
860
721
|
const promotedSnap = this.streamSnapshots.get(nextKey);
|
|
861
722
|
if (promotedSnap)
|
|
862
723
|
promotedSnap.sessionId = resolvedSessionId;
|
|
863
|
-
// Notify dashboard clients still tracking the old (pending) key via SSE
|
|
864
|
-
// so they can detect the promotion and navigate to the correct session
|
|
865
724
|
if (this._onStreamSnapshot && promotedSnap) {
|
|
866
725
|
this._onStreamSnapshot(previousKey, this.enrichSnapshot(promotedSnap));
|
|
867
726
|
}
|
|
@@ -917,10 +776,6 @@ export class Bot {
|
|
|
917
776
|
const selected = this.getSelectedSession(cs);
|
|
918
777
|
if (selected)
|
|
919
778
|
return selected;
|
|
920
|
-
// Auto-resume an existing same-thread session of this agent (back-and-forth
|
|
921
|
-
// toggling). The handover queued on `cs.pendingHandoverFrom` is intentionally
|
|
922
|
-
// dropped here — the resumed session already has its own history; replaying
|
|
923
|
-
// an external handover on top would just be duplicate context.
|
|
924
779
|
const resumed = this.findThreadSessionRuntime(chatId, cs.activeThreadId, cs.agent);
|
|
925
780
|
if (resumed) {
|
|
926
781
|
cs.pendingHandoverFrom = null;
|
|
@@ -1083,19 +938,12 @@ export class Bot {
|
|
|
1083
938
|
}
|
|
1084
939
|
return { task, interrupted: false, cancelled: false };
|
|
1085
940
|
}
|
|
1086
|
-
/**
|
|
1087
|
-
* Mark all queued tasks ahead of `targetTaskId` (in this session) so their
|
|
1088
|
-
* chain wrappers re-enqueue and yield to the steered task. Repeated steers
|
|
1089
|
-
* reset prior defer flags so only the latest target's predecessors defer.
|
|
1090
|
-
*/
|
|
1091
941
|
markQueueDeferralsForSteer(targetTaskId) {
|
|
1092
942
|
const target = this.activeTasks.get(targetTaskId);
|
|
1093
943
|
if (!target)
|
|
1094
944
|
return;
|
|
1095
945
|
const snapshot = this.streamSnapshots.get(target.sessionKey);
|
|
1096
946
|
const queuedIds = snapshot?.queuedTaskIds || [];
|
|
1097
|
-
// Reset any previous defer flags for this session's queued tasks first so
|
|
1098
|
-
// a new steer call doesn't stack on top of an earlier (now-stale) decision.
|
|
1099
947
|
for (const id of queuedIds) {
|
|
1100
948
|
const t = this.activeTasks.get(id);
|
|
1101
949
|
if (t)
|
|
@@ -1108,11 +956,6 @@ export class Bot {
|
|
|
1108
956
|
t.deferForSteer = true;
|
|
1109
957
|
}
|
|
1110
958
|
}
|
|
1111
|
-
/**
|
|
1112
|
-
* Steer hands off to the queued task's own placeholder card. Interrupt the
|
|
1113
|
-
* active task so the queued task can run next and the current preview can be
|
|
1114
|
-
* frozen in place instead of being rewritten as an error.
|
|
1115
|
-
*/
|
|
1116
959
|
async steerTaskByActionId(actionId) {
|
|
1117
960
|
const taskId = this.taskKeysByActionId.get(String(actionId));
|
|
1118
961
|
if (!taskId)
|
|
@@ -1124,10 +967,6 @@ export class Bot {
|
|
|
1124
967
|
const interrupted = this.interruptRunningTask(task.sessionKey, { freezePreview: true });
|
|
1125
968
|
return { task, interrupted, steered: false };
|
|
1126
969
|
}
|
|
1127
|
-
/**
|
|
1128
|
-
* Interrupt only the currently running task for a session, leaving queued tasks intact.
|
|
1129
|
-
* Used by the "Steer" action to let a queued task run next.
|
|
1130
|
-
*/
|
|
1131
970
|
interruptRunningTask(sessionKey, opts = {}) {
|
|
1132
971
|
const session = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
1133
972
|
if (!session)
|
|
@@ -1145,10 +984,6 @@ export class Bot {
|
|
|
1145
984
|
}
|
|
1146
985
|
return false;
|
|
1147
986
|
}
|
|
1148
|
-
/**
|
|
1149
|
-
* Return the number of tasks ahead of the given task in its session queue.
|
|
1150
|
-
* Counts running + queued (non-cancelled) tasks that were started before this one.
|
|
1151
|
-
*/
|
|
1152
987
|
getQueuePosition(sessionKey, taskId) {
|
|
1153
988
|
const session = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
1154
989
|
if (!session)
|
|
@@ -1169,20 +1004,11 @@ export class Bot {
|
|
|
1169
1004
|
return `${String(chatId)}:${String(sourceMessageId)}`;
|
|
1170
1005
|
}
|
|
1171
1006
|
queueSessionTask(session, task, taskId) {
|
|
1172
|
-
// Wrap the user task with a defer check. When steerTask() flags this task
|
|
1173
|
-
// to yield its chain slot to a steered task, the wrapper re-enqueues the
|
|
1174
|
-
// same fn at the tail and returns immediately so the next chain wrapper
|
|
1175
|
-
// (the steered task's) fires next. Tasks without a taskId (e.g. file
|
|
1176
|
-
// staging) skip the check.
|
|
1177
1007
|
const runner = async () => {
|
|
1178
1008
|
if (taskId) {
|
|
1179
1009
|
const t = this.activeTasks.get(taskId);
|
|
1180
1010
|
if (t?.deferForSteer && !t.cancelled) {
|
|
1181
1011
|
t.deferForSteer = false;
|
|
1182
|
-
// Re-enqueue at the tail. Don't await — let the current slot finish
|
|
1183
|
-
// immediately so the chain advances to the steered task. The new
|
|
1184
|
-
// wrapper preserves the original fn so the deferred task still runs
|
|
1185
|
-
// (just after the steered one).
|
|
1186
1012
|
void this.queueSessionTask(session, task, taskId);
|
|
1187
1013
|
return undefined;
|
|
1188
1014
|
}
|
|
@@ -1278,13 +1104,6 @@ export class Bot {
|
|
|
1278
1104
|
this.fireInteractionAnswered(prompt, 'cancelled');
|
|
1279
1105
|
return prompt;
|
|
1280
1106
|
}
|
|
1281
|
-
/**
|
|
1282
|
-
* Unified post-resolution hook for human-loop prompts. Each IM channel
|
|
1283
|
-
* overrides `onInteractionAnswered` to (1) collapse the original prompt card
|
|
1284
|
-
* to an answered/cancelled state and (2) echo the decision as a new chat
|
|
1285
|
-
* message so scrolling back shows what the user picked. Dashboard sessions
|
|
1286
|
-
* (chatId='dashboard') and channels that opt out remain silent.
|
|
1287
|
-
*/
|
|
1288
1107
|
fireInteractionAnswered(prompt, status) {
|
|
1289
1108
|
if (prompt.silent)
|
|
1290
1109
|
return;
|
|
@@ -1295,13 +1114,7 @@ export class Bot {
|
|
|
1295
1114
|
.then(() => this.onInteractionAnswered(prompt, summary))
|
|
1296
1115
|
.catch(err => this.warn(`onInteractionAnswered failed: ${err?.message || err}`));
|
|
1297
1116
|
}
|
|
1298
|
-
/**
|
|
1299
|
-
* Channel hook fired after a human-loop prompt resolves (answered or
|
|
1300
|
-
* cancelled). Default: no-op. Override in channel subclasses to update the
|
|
1301
|
-
* original card and post a decision-echo message.
|
|
1302
|
-
*/
|
|
1303
1117
|
async onInteractionAnswered(_prompt, _summary) {
|
|
1304
|
-
// Default: no-op.
|
|
1305
1118
|
}
|
|
1306
1119
|
emitInteractionResolved(taskId, promptId) {
|
|
1307
1120
|
const task = this.activeTasks.get(taskId);
|
|
@@ -1356,14 +1169,6 @@ export class Bot {
|
|
|
1356
1169
|
else
|
|
1357
1170
|
this.humanLoopPromptIdsByChat.delete(chatKey);
|
|
1358
1171
|
}
|
|
1359
|
-
/**
|
|
1360
|
-
* Create an interaction handler that bridges agent requests to the human-loop
|
|
1361
|
-
* state machine and pushes SSE events to the dashboard.
|
|
1362
|
-
*
|
|
1363
|
-
* IM channel subclasses override `renderInteractionPrompt()` to render
|
|
1364
|
-
* buttons/cards in their native UI. Dashboard clients receive the
|
|
1365
|
-
* `interaction` SSE event and respond via REST.
|
|
1366
|
-
*/
|
|
1367
1172
|
createInteractionHandler(chatId, taskId) {
|
|
1368
1173
|
return async (request) => {
|
|
1369
1174
|
const active = this.beginHumanLoopPrompt({
|
|
@@ -1382,19 +1187,9 @@ export class Bot {
|
|
|
1382
1187
|
questions: request.questions,
|
|
1383
1188
|
currentIndex: active.prompt.currentIndex,
|
|
1384
1189
|
};
|
|
1385
|
-
// Resolve sessionKey live at emit time — the task entry tracks promotion
|
|
1386
|
-
// (pending → native id), so a key captured at handler-creation time would
|
|
1387
|
-
// go stale on the very first turn of a fresh session and the dashboard
|
|
1388
|
-
// SSE event would land on an already-moved snapshot.
|
|
1389
1190
|
const task = this.activeTasks.get(taskId);
|
|
1390
1191
|
if (task)
|
|
1391
1192
|
this.emitStream(task.sessionKey, { type: 'interaction', taskId, interaction: interactionSnapshot });
|
|
1392
|
-
// Dashboard sessions reply through SSE + REST (no IM render). When an IM
|
|
1393
|
-
// bot is also attached, its renderInteractionPrompt override would still
|
|
1394
|
-
// fire here with chatId='dashboard' — sending the prompt to the IM API
|
|
1395
|
-
// with an invalid receive_id, which surfaces as "Request failed with
|
|
1396
|
-
// status code 400" from the axios-based SDK. Skip the render for
|
|
1397
|
-
// dashboard chats; the SSE event is the canonical delivery.
|
|
1398
1193
|
if (chatId !== 'dashboard') {
|
|
1399
1194
|
try {
|
|
1400
1195
|
await this.renderInteractionPrompt(active.prompt, chatId);
|
|
@@ -1407,31 +1202,15 @@ export class Bot {
|
|
|
1407
1202
|
return active.result;
|
|
1408
1203
|
};
|
|
1409
1204
|
}
|
|
1410
|
-
/**
|
|
1411
|
-
* Render an interaction prompt in the IM channel.
|
|
1412
|
-
* Override in channel subclasses (Telegram, Feishu, etc.).
|
|
1413
|
-
* Dashboard-only sessions (chatId='dashboard') are a no-op by default.
|
|
1414
|
-
*/
|
|
1415
1205
|
async renderInteractionPrompt(_prompt, _chatId) {
|
|
1416
|
-
// Default: no-op (dashboard-only sessions use SSE events instead)
|
|
1417
1206
|
}
|
|
1418
|
-
// ---- Public interaction API (used by dashboard routes) --------------------
|
|
1419
|
-
/** Respond to a pending interaction prompt with a selected option. */
|
|
1420
1207
|
interactionSelectOption(promptId, optionValue, opts) {
|
|
1421
1208
|
return this.humanLoopSelectOption(promptId, optionValue, opts);
|
|
1422
1209
|
}
|
|
1423
|
-
/** Submit freeform text to a pending interaction prompt. */
|
|
1424
1210
|
interactionSubmitText(promptId, text) {
|
|
1425
1211
|
const prompt = this.humanLoopPrompt(promptId);
|
|
1426
1212
|
if (!prompt)
|
|
1427
1213
|
return null;
|
|
1428
|
-
// The dashboard modal submits a custom answer via an explicit Submit button,
|
|
1429
|
-
// so accept the text whenever the current question PERMITS freeform — not
|
|
1430
|
-
// only after an "Other" chip flipped `awaitingFreeform` (the IM-card flow,
|
|
1431
|
-
// which `isHumanLoopAwaitingText` gates on). An options question only allows
|
|
1432
|
-
// it when `allowFreeform` is set; an option-less question is freeform by
|
|
1433
|
-
// definition. (The IM passive-text path keeps the stricter check so a normal
|
|
1434
|
-
// chat message isn't silently captured as an answer.)
|
|
1435
1214
|
const question = currentHumanLoopQuestion(prompt);
|
|
1436
1215
|
if (!question)
|
|
1437
1216
|
return null;
|
|
@@ -1444,15 +1223,12 @@ export class Bot {
|
|
|
1444
1223
|
this.resolveHumanLoopPrompt(prompt.promptId);
|
|
1445
1224
|
return { prompt, ...result };
|
|
1446
1225
|
}
|
|
1447
|
-
/** Skip the current question in a pending interaction prompt. */
|
|
1448
1226
|
interactionSkip(promptId) {
|
|
1449
1227
|
return this.humanLoopSkip(promptId);
|
|
1450
1228
|
}
|
|
1451
|
-
/** Cancel a pending interaction prompt. */
|
|
1452
1229
|
interactionCancel(promptId, reason = 'Cancelled from dashboard.') {
|
|
1453
1230
|
return this.humanLoopCancel(promptId, reason);
|
|
1454
1231
|
}
|
|
1455
|
-
/** Get a specific interaction prompt by ID. */
|
|
1456
1232
|
interactionPrompt(promptId) {
|
|
1457
1233
|
return this.humanLoopPrompt(promptId);
|
|
1458
1234
|
}
|
|
@@ -1465,7 +1241,6 @@ export class Bot {
|
|
|
1465
1241
|
sessionId: opts.sessionId,
|
|
1466
1242
|
workdir: opts.workdir,
|
|
1467
1243
|
workspacePath: null,
|
|
1468
|
-
// Only override when explicitly provided — undefined skips the overwrite in upsertSessionRuntime
|
|
1469
1244
|
...(opts.modelId !== undefined ? { modelId: opts.modelId } : {}),
|
|
1470
1245
|
...(opts.thinkingEffort !== undefined ? { thinkingEffort: opts.thinkingEffort } : {}),
|
|
1471
1246
|
...(opts.handoverFrom !== undefined ? { handoverFrom: opts.handoverFrom } : {}),
|
|
@@ -1493,12 +1268,7 @@ export class Bot {
|
|
|
1493
1268
|
this.finishTask(taskId);
|
|
1494
1269
|
return;
|
|
1495
1270
|
}
|
|
1496
|
-
// Thread the per-send Workflow choice so the live divider folds to `ultra`
|
|
1497
|
-
// immediately (the dashboard composer picks ultra per-send without flipping
|
|
1498
|
-
// the agent-global flag resolveSessionStreamConfig would otherwise read).
|
|
1499
1271
|
this.emitStreamStart(taskId, session, { workflowEnabled: opts.workflowEnabled });
|
|
1500
|
-
// Wire up IM rendering for non-dashboard chats so /goal-driven tasks stream
|
|
1501
|
-
// to the same channel that submitted them, matching handleMessage's UX.
|
|
1502
1272
|
const presenter = chatId !== 'dashboard'
|
|
1503
1273
|
? await this.createImTaskPresenter({
|
|
1504
1274
|
chatId, taskId, session, agent: session.agent, prompt, attachments,
|
|
@@ -1562,28 +1332,9 @@ export class Bot {
|
|
|
1562
1332
|
});
|
|
1563
1333
|
return { ok: true, taskId, sessionKey: session.key, queued: true };
|
|
1564
1334
|
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Channel hook — returns a presenter that streams the task's runStream
|
|
1567
|
-
* output to the IM chat that submitted it. Default: null (dashboard-only
|
|
1568
|
-
* chats and channels that haven't opted in stay silent in IM).
|
|
1569
|
-
*/
|
|
1570
1335
|
async createImTaskPresenter(_opts) {
|
|
1571
1336
|
return null;
|
|
1572
1337
|
}
|
|
1573
|
-
/**
|
|
1574
|
-
* Goal continuation: after a turn ends, if a goal is still active for the
|
|
1575
|
-
* session, account token + wall-clock usage, then enqueue one more task with
|
|
1576
|
-
* the rendered continuation prompt. If the budget was just crossed, enqueue a
|
|
1577
|
-
* single wrap-up turn with the budget-limit prompt instead. Goal-continuation
|
|
1578
|
-
* tasks that get cancelled or errored auto-pause the goal so the loop does
|
|
1579
|
-
* not silently resume on the user's next message.
|
|
1580
|
-
*
|
|
1581
|
-
* Codex and Claude sessions short-circuit: each runs its own native `/goal`
|
|
1582
|
-
* lifecycle (codex's app-server state machine; claude's in-process Stop
|
|
1583
|
-
* hook), so pikiloom stays out to avoid a double loop. See setSessionGoal
|
|
1584
|
-
* et al — they bridge to codex's `thread/goal/*` RPC and to claude's
|
|
1585
|
-
* `/goal <condition>` slash command instead of writing pikiloom's goal.json.
|
|
1586
|
-
*/
|
|
1587
1338
|
maybeEnqueueGoalContinuation(session, opts, result) {
|
|
1588
1339
|
if (session.agent === 'codex' || session.agent === 'claude')
|
|
1589
1340
|
return;
|
|
@@ -1648,16 +1399,6 @@ export class Bot {
|
|
|
1648
1399
|
goalContinuation: { kind: 'continuation', goalId: updated.goalId },
|
|
1649
1400
|
});
|
|
1650
1401
|
}
|
|
1651
|
-
/**
|
|
1652
|
-
* Normalized goal view used by IM/dashboard renderers — same shape regardless
|
|
1653
|
-
* of whether the source is pikiloom's goal.json (claude / gemini / …) or
|
|
1654
|
-
* codex's native SQLite state machine.
|
|
1655
|
-
*/
|
|
1656
|
-
// SessionGoalView is exported below the class.
|
|
1657
|
-
/**
|
|
1658
|
-
* Read the current goal for a session. For codex this hits codex's native
|
|
1659
|
-
* `thread/goal/get`; for other drivers, reads goal.json.
|
|
1660
|
-
*/
|
|
1661
1402
|
async getSessionGoal(workdir, agent, sessionId) {
|
|
1662
1403
|
if (agent === 'codex') {
|
|
1663
1404
|
if (!sessionId || isPendingSessionId(sessionId))
|
|
@@ -1674,12 +1415,6 @@ export class Bot {
|
|
|
1674
1415
|
const goal = readGoal(workdir, agent, sessionId);
|
|
1675
1416
|
return goal ? normalizeFromPikiloom(goal) : null;
|
|
1676
1417
|
}
|
|
1677
|
-
/**
|
|
1678
|
-
* Set (or replace) the goal for a session. For codex this routes through
|
|
1679
|
-
* codex's native `thread/goal/set` and codex auto-starts a continuation turn
|
|
1680
|
-
* internally. For other drivers, pikiloom writes goal.json and enqueues the
|
|
1681
|
-
* first continuation turn so the agent starts working immediately.
|
|
1682
|
-
*/
|
|
1683
1418
|
async setSessionGoal(workdir, agent, sessionId, opts) {
|
|
1684
1419
|
if (agent === 'codex') {
|
|
1685
1420
|
if (!sessionId || isPendingSessionId(sessionId)) {
|
|
@@ -1693,7 +1428,6 @@ export class Bot {
|
|
|
1693
1428
|
});
|
|
1694
1429
|
if (!resp.ok)
|
|
1695
1430
|
throw new Error(resp.error);
|
|
1696
|
-
// codex returns a snapshot; if for some reason it's null, re-fetch.
|
|
1697
1431
|
const goal = resp.goal ?? (await getCodexGoal(sessionId));
|
|
1698
1432
|
if (!goal)
|
|
1699
1433
|
throw new Error('codex did not return a goal snapshot');
|
|
@@ -1703,12 +1437,6 @@ export class Bot {
|
|
|
1703
1437
|
if (!sessionId || isPendingSessionId(sessionId)) {
|
|
1704
1438
|
throw new Error('claude session must exist before /goal — send a first message to create the transcript');
|
|
1705
1439
|
}
|
|
1706
|
-
// Native /goal owns its own continuation engine (Stop hook). pikiloom
|
|
1707
|
-
// just submits the slash command as the next task; claude internally
|
|
1708
|
-
// sets up the goal_status attachment, injects its meta directive, and
|
|
1709
|
-
// keeps looping until the Haiku completion check returns met. Token
|
|
1710
|
-
// budget is accepted in the API for shape parity with codex/portable
|
|
1711
|
-
// but ignored — claude native /goal has no budget concept.
|
|
1712
1440
|
const objective = opts.objective.trim();
|
|
1713
1441
|
if (!objective)
|
|
1714
1442
|
throw new Error('objective must be non-empty');
|
|
@@ -1721,8 +1449,6 @@ export class Bot {
|
|
|
1721
1449
|
modelId: opts.modelId,
|
|
1722
1450
|
thinkingEffort: opts.thinkingEffort,
|
|
1723
1451
|
});
|
|
1724
|
-
// Return an optimistic snapshot — the actual goal_status attachment is
|
|
1725
|
-
// written by claude during the task; readers can poll getSessionGoal.
|
|
1726
1452
|
return normalizeFromClaudeNative({
|
|
1727
1453
|
condition: objective,
|
|
1728
1454
|
status: 'active',
|
|
@@ -1760,8 +1486,6 @@ export class Bot {
|
|
|
1760
1486
|
return goal ? normalizeFromCodex(goal) : null;
|
|
1761
1487
|
}
|
|
1762
1488
|
if (agent === 'claude') {
|
|
1763
|
-
// Claude's native /goal exposes no pause/resume — only set and clear.
|
|
1764
|
-
// Surface a clear error so the IM layer can render a friendly message.
|
|
1765
1489
|
throw new Error('Claude native /goal does not support pause/resume — only `/goal clear`. Re-issue `/goal <objective>` to start fresh.');
|
|
1766
1490
|
}
|
|
1767
1491
|
const goal = pauseGoal(workdir, agent, sessionId);
|
|
@@ -1810,7 +1534,6 @@ export class Bot {
|
|
|
1810
1534
|
if (agent === 'claude') {
|
|
1811
1535
|
if (!sessionId || isPendingSessionId(sessionId))
|
|
1812
1536
|
return;
|
|
1813
|
-
// Read goal-status first to avoid spawning a no-op turn when nothing is set.
|
|
1814
1537
|
const existing = getClaudeNativeGoal(workdir, sessionId);
|
|
1815
1538
|
if (!existing)
|
|
1816
1539
|
return;
|
|
@@ -1854,24 +1577,9 @@ export class Bot {
|
|
|
1854
1577
|
const interrupted = this.interruptRunningTask(task.sessionKey, { freezePreview: true });
|
|
1855
1578
|
return { task, interrupted, steered: interrupted || !!task };
|
|
1856
1579
|
}
|
|
1857
|
-
/**
|
|
1858
|
-
* Stop only the currently running task for a session. Queued tasks are
|
|
1859
|
-
* intentionally left intact and run normally once the chain advances — the
|
|
1860
|
-
* stop button means "abort what's running right now", not "throw away the
|
|
1861
|
-
* queue". To drop a specific queued entry, use the per-row × button which
|
|
1862
|
-
* routes through `cancelTask`.
|
|
1863
|
-
*/
|
|
1864
1580
|
stopAllSessionTasks(sessionKey) {
|
|
1865
1581
|
return this.stopTasksForSession(sessionKey);
|
|
1866
1582
|
}
|
|
1867
|
-
/**
|
|
1868
|
-
* Public "start a fresh session" entry point — wired to the "+ New" button
|
|
1869
|
-
* and the `/new` command. Only clears the chat's session selection so the
|
|
1870
|
-
* next user message lands in a fresh session; the previously selected
|
|
1871
|
-
* session keeps running independently (matching dashboard behaviour, where
|
|
1872
|
-
* each session is its own card and is never aborted by creating another).
|
|
1873
|
-
* Use `cancelTask` / `/stop` to actually interrupt a running task.
|
|
1874
|
-
*/
|
|
1875
1583
|
resetConversationForChat(chatId) {
|
|
1876
1584
|
const cs = this.chat(chatId);
|
|
1877
1585
|
this.resetChatConversation(cs);
|
|
@@ -1881,13 +1589,6 @@ export class Bot {
|
|
|
1881
1589
|
this.adoptSession(cs, session);
|
|
1882
1590
|
return this.getSelectedSession(cs);
|
|
1883
1591
|
}
|
|
1884
|
-
/**
|
|
1885
|
-
* Resume an existing session in a chat and restore the agent's persistent
|
|
1886
|
-
* model / effort / BYOK Profile binding so the next stream — and the IM
|
|
1887
|
-
* picker chips — match the session that was just adopted. This is the
|
|
1888
|
-
* shared "click a row from the workspace list" path used by both the
|
|
1889
|
-
* interactive selector and the text-command `/sessions <#>` flow.
|
|
1890
|
-
*/
|
|
1891
1592
|
resumeSessionForChat(chatId, session) {
|
|
1892
1593
|
const runtime = this.adoptExistingSessionForChat(chatId, session);
|
|
1893
1594
|
if (session.model) {
|
|
@@ -1905,15 +1606,9 @@ export class Bot {
|
|
|
1905
1606
|
const cs = this.chat(chatId);
|
|
1906
1607
|
if (cs.agent === agent)
|
|
1907
1608
|
return false;
|
|
1908
|
-
// Capture the live session of the *outgoing* agent so the next message to
|
|
1909
|
-
// the new agent can replay it as a handover. We capture BEFORE flipping
|
|
1910
|
-
// cs.agent so the ref is honest about which agent it points at.
|
|
1911
1609
|
const prevAgent = cs.agent;
|
|
1912
1610
|
const prevSessionId = cs.sessionId && !isPendingSessionId(cs.sessionId) ? cs.sessionId : null;
|
|
1913
1611
|
cs.agent = agent;
|
|
1914
|
-
// Pre-existing session of the new agent in this thread — back-and-forth
|
|
1915
|
-
// toggling resumes it without handover. The user's intent is "continue what
|
|
1916
|
-
// I had", not "translate cross-agent".
|
|
1917
1612
|
const resumed = this.findThreadSessionRuntime(chatId, cs.activeThreadId, agent);
|
|
1918
1613
|
if (resumed) {
|
|
1919
1614
|
cs.pendingHandoverFrom = null;
|
|
@@ -1921,11 +1616,6 @@ export class Bot {
|
|
|
1921
1616
|
this.log(`agent switched to ${agent} chat=${chatId} resumed=${resumed.sessionId}`);
|
|
1922
1617
|
return true;
|
|
1923
1618
|
}
|
|
1924
|
-
// No existing session of the new agent → next message will stage a fresh
|
|
1925
|
-
// one. Park the outgoing session as the handover source. If the outgoing
|
|
1926
|
-
// agent had no live session (e.g. the user is rapidly toggling agents
|
|
1927
|
-
// before sending anything), keep any already-pending handover so the
|
|
1928
|
-
// original source isn't lost across intermediate switches.
|
|
1929
1619
|
if (prevSessionId) {
|
|
1930
1620
|
cs.pendingHandoverFrom = { agent: prevAgent, sessionId: prevSessionId };
|
|
1931
1621
|
}
|
|
@@ -1933,26 +1623,8 @@ export class Bot {
|
|
|
1933
1623
|
this.log(`agent switched to ${agent} chat=${chatId} handoverFrom=${describeHandoverRef(cs.pendingHandoverFrom)}`);
|
|
1934
1624
|
return true;
|
|
1935
1625
|
}
|
|
1936
|
-
/**
|
|
1937
|
-
* Switch the active model for a chat. Supports both native (agent CLI's own
|
|
1938
|
-
* auth) and BYOK Profile selections:
|
|
1939
|
-
* - `profileId === undefined` (default) — set native model only; pre-union
|
|
1940
|
-
* callers (text-command channels) keep working unchanged.
|
|
1941
|
-
* - `profileId === null` — explicit clear: drop any active Profile, fall
|
|
1942
|
-
* back to native model.
|
|
1943
|
-
* - `profileId === '<uuid>'` — bind that Profile; `modelId` should match
|
|
1944
|
-
* the Profile's modelId so display surfaces stay in sync.
|
|
1945
|
-
*
|
|
1946
|
-
* The native model field (`agentConfigs[agent].model`) always tracks the
|
|
1947
|
-
* effective model id used by the agent CLI — when a Profile is bound, this
|
|
1948
|
-
* lets `modelForAgent()` return the right display string without an extra
|
|
1949
|
-
* lookup. When unbinding, we leave the field alone so the user's prior
|
|
1950
|
-
* native pick is preserved.
|
|
1951
|
-
*/
|
|
1952
1626
|
switchModelForChat(chatId, modelId, profileId) {
|
|
1953
1627
|
const cs = this.chat(chatId);
|
|
1954
|
-
// Update activeProfileByAgent first — resolveSessionStreamConfig downstream
|
|
1955
|
-
// reads it via getActiveProfile() during spawn.
|
|
1956
1628
|
if (profileId !== undefined) {
|
|
1957
1629
|
setActiveProfile(cs.agent, profileId || null);
|
|
1958
1630
|
}
|
|
@@ -1969,25 +1641,11 @@ export class Bot {
|
|
|
1969
1641
|
: ' profile=(cleared)';
|
|
1970
1642
|
this.log(`model switched to ${modelId} for ${cs.agent} chat=${chatId} session=${cs.activeSessionKey || '(none)'}${profileTag}`);
|
|
1971
1643
|
}
|
|
1972
|
-
/**
|
|
1973
|
-
* The Profile id currently bound to this agent, if any. Used by the IM
|
|
1974
|
-
* picker to flag "current selection" when the user has a Profile bound —
|
|
1975
|
-
* since multiple Profiles may share the same modelId, a model-id match
|
|
1976
|
-
* alone is ambiguous.
|
|
1977
|
-
*/
|
|
1978
1644
|
activeProfileIdForAgent(agent) {
|
|
1979
1645
|
return getActiveProfileId(agent);
|
|
1980
1646
|
}
|
|
1981
1647
|
switchEffortForChat(chatId, effort) {
|
|
1982
1648
|
const cs = this.chat(chatId);
|
|
1983
|
-
// "ultra" is a synthetic top rung in the effort picker, NOT a real --effort
|
|
1984
|
-
// value (the claude CLI rejects anything outside low|medium|high|xhigh|max).
|
|
1985
|
-
// It bundles "max reasoning depth + permit multi-agent Workflow
|
|
1986
|
-
// orchestration" — the same pairing as Claude's own `ultracode` mode. Decode
|
|
1987
|
-
// it here, the single apply choke point, so the rest of the pipeline only
|
|
1988
|
-
// ever sees a concrete effort value plus the orthogonal workflow flag.
|
|
1989
|
-
// Because the rungs are mutually exclusive, picking any concrete level also
|
|
1990
|
-
// clears the workflow opt-in (capability-gated — only claude advertises it).
|
|
1991
1649
|
const ultra = effort === 'ultra';
|
|
1992
1650
|
const realEffort = ultra ? 'max' : effort;
|
|
1993
1651
|
this.setEffortForAgent(cs.agent, realEffort);
|
|
@@ -2001,12 +1659,6 @@ export class Bot {
|
|
|
2001
1659
|
}
|
|
2002
1660
|
this.log(`effort switched to ${effort} (effort=${realEffort}, workflow=${ultra}) for ${cs.agent} chat=${chatId}`);
|
|
2003
1661
|
}
|
|
2004
|
-
/**
|
|
2005
|
-
* Effort value to *display* in the picker. Workflow is orthogonal under the
|
|
2006
|
-
* hood, but the UI folds "max depth + workflow on" into the single synthetic
|
|
2007
|
-
* `ultra` rung (see {@link switchEffortForChat}), so report it as current when
|
|
2008
|
-
* the agent has orchestration enabled. Mirrors the decomposition above.
|
|
2009
|
-
*/
|
|
2010
1662
|
effortSelectionForAgent(agent) {
|
|
2011
1663
|
const effort = this.effortForAgent(agent);
|
|
2012
1664
|
if (!effort)
|
|
@@ -2023,13 +1675,6 @@ export class Bot {
|
|
|
2023
1675
|
this.log(`permission mode switched to ${mode} for claude chat=${chatId}`);
|
|
2024
1676
|
}
|
|
2025
1677
|
}
|
|
2026
|
-
/**
|
|
2027
|
-
* Toggle multi-agent Workflow orchestration for the chat's current agent.
|
|
2028
|
-
* Unlike permission-mode it does NOT reset the conversation — the tool set is
|
|
2029
|
-
* resolved per-invocation, so the change cleanly takes effect on the next
|
|
2030
|
-
* turn without invalidating the session transcript. No-op for agents whose
|
|
2031
|
-
* driver doesn't advertise the capability.
|
|
2032
|
-
*/
|
|
2033
1678
|
switchWorkflowForChat(chatId, enabled) {
|
|
2034
1679
|
const cs = this.chat(chatId);
|
|
2035
1680
|
if (!getDriverCapabilities(cs.agent).workflow) {
|
|
@@ -2041,12 +1686,6 @@ export class Bot {
|
|
|
2041
1686
|
this.log(`workflow ${enabled ? 'enabled' : 'disabled'} for ${cs.agent} chat=${chatId}`);
|
|
2042
1687
|
}
|
|
2043
1688
|
modelForAgent(agent) {
|
|
2044
|
-
// For agents whose CLIs cannot switch model via flags (Hermes uses ACP
|
|
2045
|
-
// session/set_model, which only fires when a BYOK Profile is bound), the
|
|
2046
|
-
// active Profile is the only meaningful source of truth — falling back to
|
|
2047
|
-
// `agentConfigs[agent].model` would surface a stale value the runtime
|
|
2048
|
-
// never actually uses. For agents with native model selectors
|
|
2049
|
-
// (Claude/Codex/Gemini), the user-config field is still authoritative.
|
|
2050
1689
|
if (agent === 'hermes') {
|
|
2051
1690
|
const bound = getAgentBoundModelId('hermes');
|
|
2052
1691
|
if (bound)
|
|
@@ -2054,11 +1693,6 @@ export class Bot {
|
|
|
2054
1693
|
}
|
|
2055
1694
|
return this.agentConfigs[agent]?.model || '';
|
|
2056
1695
|
}
|
|
2057
|
-
/**
|
|
2058
|
-
* Resolve the effective model + thinking effort that a stream for `cs` will run with.
|
|
2059
|
-
* Mirrors the fallback chain used inside runStream() so callers (e.g. submitSessionTask
|
|
2060
|
-
* emitting a 'start' event) can label the active turn before runStream resolves it.
|
|
2061
|
-
*/
|
|
2062
1696
|
resolveSessionStreamConfig(cs, opts) {
|
|
2063
1697
|
const agentConfig = this.agentConfigs[cs.agent] || {};
|
|
2064
1698
|
const sessionWorkdir = cs.workdir || this.workdir;
|
|
@@ -2074,12 +1708,6 @@ export class Bot {
|
|
|
2074
1708
|
|| agentConfig.reasoningEffort
|
|
2075
1709
|
|| 'high';
|
|
2076
1710
|
const effort = cs.agent === 'gemini' ? null : (effortRaw || null);
|
|
2077
|
-
// Fold to the synthetic 'ultra' rung for display when Workflow is on (mirrors
|
|
2078
|
-
// effortSelectionForAgent / the dashboard's foldUltraEffort), so the live reply
|
|
2079
|
-
// badge and IM running footer label the turn 'ultra' instead of a bare 'max'.
|
|
2080
|
-
// Prefer the per-turn workflow choice when the caller threads one (dashboard
|
|
2081
|
-
// composer sends ultra per-send without flipping the agent-global flag);
|
|
2082
|
-
// fall back to the agent-global flag (IM /mode, agent card).
|
|
2083
1711
|
const workflowOn = opts?.workflowEnabled ?? this.workflowEnabledForAgent(cs.agent);
|
|
2084
1712
|
const displayEffort = effort && getDriverCapabilities(cs.agent).workflow && workflowOn
|
|
2085
1713
|
? 'ultra'
|
|
@@ -2102,9 +1730,6 @@ export class Bot {
|
|
|
2102
1730
|
}
|
|
2103
1731
|
fetchModels(agent, workdir) {
|
|
2104
1732
|
const wd = workdir || this.workdir;
|
|
2105
|
-
// Provider-aware: when the agent is bound to a BYOK Profile, the
|
|
2106
|
-
// returned model list is the provider's enumerable models. This keeps
|
|
2107
|
-
// IM /models consistent with the dashboard agent card.
|
|
2108
1733
|
return resolveAgentModels(agent, { workdir: wd, currentModel: this.modelForAgent(agent) });
|
|
2109
1734
|
}
|
|
2110
1735
|
setDefaultAgent(agent) {
|
|
@@ -2143,12 +1768,6 @@ export class Bot {
|
|
|
2143
1768
|
config.workflowEnabled = enabled;
|
|
2144
1769
|
this.log(`workflow for ${agent} changed to ${enabled}`);
|
|
2145
1770
|
}
|
|
2146
|
-
/**
|
|
2147
|
-
* Switch Claude's access mode (subscription TUI vs `claude -p` Agent SDK
|
|
2148
|
-
* credits). Persisted preference — takes effect on the NEXT spawned turn
|
|
2149
|
-
* (in-flight streams keep their own opts); does not reset any conversation
|
|
2150
|
-
* since both modes resume the same native session transcript.
|
|
2151
|
-
*/
|
|
2152
1771
|
setClaudeAccessMode(mode) {
|
|
2153
1772
|
const config = this.agentConfigs.claude;
|
|
2154
1773
|
if (config)
|
|
@@ -2157,13 +1776,8 @@ export class Bot {
|
|
|
2157
1776
|
}
|
|
2158
1777
|
persistAgentPreference(agent, kind, value) {
|
|
2159
1778
|
try {
|
|
2160
|
-
// Hermes model writes go to the active BYOK Profile (the runtime's only
|
|
2161
|
-
// model-switching surface). Falls through to the legacy `hermesModel`
|
|
2162
|
-
// user-config field when no Profile is bound.
|
|
2163
1779
|
if (kind === 'model' && agent === 'hermes' && setAgentBoundModelId('hermes', value))
|
|
2164
1780
|
return;
|
|
2165
|
-
// Workflow orchestration opt-in is a boolean field, and only claude
|
|
2166
|
-
// advertises the capability, so it bypasses the string patch below.
|
|
2167
1781
|
if (kind === 'workflow') {
|
|
2168
1782
|
if (agent === 'claude')
|
|
2169
1783
|
updateUserConfig({ claudeWorkflowEnabled: value === '1' });
|
|
@@ -2286,34 +1900,13 @@ export class Bot {
|
|
|
2286
1900
|
}
|
|
2287
1901
|
afterSwitchWorkdir(_oldPath, _newPath) { }
|
|
2288
1902
|
onManagedConfigChange(_config, _opts = {}) { }
|
|
2289
|
-
/**
|
|
2290
|
-
* Subclass entry point — connect to the channel and block on its
|
|
2291
|
-
* listen loop. Each channel implementation overrides this; calling it
|
|
2292
|
-
* on the base class is a programming error.
|
|
2293
|
-
*/
|
|
2294
1903
|
run() {
|
|
2295
1904
|
throw new Error('Bot.run() must be implemented by a channel subclass');
|
|
2296
1905
|
}
|
|
2297
|
-
/**
|
|
2298
|
-
* Subclass hook: tear down the channel transport so `run()` can resolve.
|
|
2299
|
-
* Subclasses override to disconnect their specific channel — the base
|
|
2300
|
-
* implementation only cleans up the bot-level subscriptions that don't
|
|
2301
|
-
* belong to any one channel.
|
|
2302
|
-
*
|
|
2303
|
-
* Used by ChannelSupervisor when a channel must be stopped or replaced
|
|
2304
|
-
* in-process (channel removal, credential rotation) without restarting
|
|
2305
|
-
* the entire pikiloom runtime.
|
|
2306
|
-
*/
|
|
2307
1906
|
requestStop() {
|
|
2308
1907
|
this.userConfigUnsubscribe?.();
|
|
2309
1908
|
this.userConfigUnsubscribe = null;
|
|
2310
1909
|
}
|
|
2311
|
-
/**
|
|
2312
|
-
* Scan registered workspaces + the active workdir for sessions stuck in
|
|
2313
|
-
* 'running' state after a crash/restart and downgrade them to 'incomplete'.
|
|
2314
|
-
* Safe to call at any time — only touches records whose owning process is
|
|
2315
|
-
* no longer alive (or that have gone stale past the age threshold).
|
|
2316
|
-
*/
|
|
2317
1910
|
reconcileStaleRunningSessions() {
|
|
2318
1911
|
const seen = new Set();
|
|
2319
1912
|
const candidates = [this.workdir];
|
|
@@ -2344,10 +1937,6 @@ export class Bot {
|
|
|
2344
1937
|
else if (nextWorkdir !== this.workdir) {
|
|
2345
1938
|
this.switchWorkdir(nextWorkdir, { persist: false });
|
|
2346
1939
|
}
|
|
2347
|
-
// The configured value is a *preference* (baseline 'codex' when unset);
|
|
2348
|
-
// clamp it to an installed agent so a fresh machine whose preferred CLI
|
|
2349
|
-
// isn't installed still routes new conversations to one that can actually
|
|
2350
|
-
// run, instead of surfacing an uninstalled default.
|
|
2351
1940
|
const nextDefaultAgent = resolveDefaultAgent(config.defaultAgent || 'codex', listAgents().agents);
|
|
2352
1941
|
if (opts.initial)
|
|
2353
1942
|
this.defaultAgent = nextDefaultAgent;
|
|
@@ -2368,10 +1957,6 @@ export class Bot {
|
|
|
2368
1957
|
else
|
|
2369
1958
|
this.setEffortForAgent(agent, nextEffort);
|
|
2370
1959
|
}
|
|
2371
|
-
// Access mode (claude only) IS reconciled — unlike workflow, it's a
|
|
2372
|
-
// persisted preference, so an external setting.json edit or a dashboard
|
|
2373
|
-
// save (which both flow through here via onUserConfigChange) must push
|
|
2374
|
-
// the new value onto the running bot so the next turn spawns accordingly.
|
|
2375
1960
|
if (agent === 'claude') {
|
|
2376
1961
|
const nextAccessMode = resolveClaudeAccessMode(config);
|
|
2377
1962
|
if (this.claudeAccessMode !== nextAccessMode) {
|
|
@@ -2381,16 +1966,12 @@ export class Bot {
|
|
|
2381
1966
|
this.setClaudeAccessMode(nextAccessMode);
|
|
2382
1967
|
}
|
|
2383
1968
|
}
|
|
2384
|
-
// Workflow is intentionally NOT reconciled from config here: it's an
|
|
2385
|
-
// in-memory per-session toggle (composer / IM /mode), so a config-sync
|
|
2386
|
-
// tick must not clobber a deliberate in-session choice.
|
|
2387
1969
|
}
|
|
2388
1970
|
if (!opts.initial)
|
|
2389
1971
|
this.onManagedConfigChange(config, opts);
|
|
2390
1972
|
}
|
|
2391
1973
|
async runStream(prompt, cs, attachments, onText, systemPrompt, mcpSendFile, abortSignal, onInteraction, onSteerReady, onCodexTurnReady, extras) {
|
|
2392
1974
|
const agentConfig = this.agentConfigs[cs.agent] || {};
|
|
2393
|
-
// Session-level config stored on disk — used as fallback between explicit override and global defaults
|
|
2394
1975
|
const sessionWorkdirForConfig = 'workdir' in cs && typeof cs.workdir === 'string' && cs.workdir ? cs.workdir : this.workdir;
|
|
2395
1976
|
const storedConfig = cs.sessionId && !isPendingSessionId(cs.sessionId)
|
|
2396
1977
|
? getSessionStoredConfig(sessionWorkdirForConfig, cs.agent, cs.sessionId)
|
|
@@ -2407,11 +1988,6 @@ export class Bot {
|
|
|
2407
1988
|
this.debug(`[runStream] agent=${cs.agent} session=${cs.sessionId || '(new)'} workdir=${sessionWorkdir} timeout=${this.runTimeout}s attachments=${attachments.length}`);
|
|
2408
1989
|
this.debug(`[runStream] ${cs.agent} config: model=${resolvedModel} extraArgs=[${extraArgs.join(' ')}]`);
|
|
2409
1990
|
const isFirstTurnOfSession = !cs.sessionId || isPendingSessionId(cs.sessionId);
|
|
2410
|
-
// ── Cross-agent handover ──
|
|
2411
|
-
// First turn of a session created by an agent switch: read the prior agent's
|
|
2412
|
-
// session, compact it, and prepend the seed to this turn's prompt. After this
|
|
2413
|
-
// single injection the new agent owns the canonical session file and `--resume`
|
|
2414
|
-
// takes over. See agent/handover.ts.
|
|
2415
1991
|
const handoverFrom = ('handoverFrom' in cs && cs.handoverFrom) ? cs.handoverFrom : null;
|
|
2416
1992
|
if (isFirstTurnOfSession && handoverFrom) {
|
|
2417
1993
|
try {
|
|
@@ -2437,32 +2013,10 @@ export class Bot {
|
|
|
2437
2013
|
this.warn(`[runStream] handover threw: ${e?.message || e}; proceeding without prior context`);
|
|
2438
2014
|
}
|
|
2439
2015
|
}
|
|
2440
|
-
// Per-turn workflow opt-in (dashboard composer passes it explicitly);
|
|
2441
|
-
// falls back to the agent's in-memory flag (IM /mode) when unspecified.
|
|
2442
|
-
// Default off — never read from a persisted config default.
|
|
2443
2016
|
const workflowEnabled = cs.agent === 'claude' && (extras?.workflowEnabled ?? this.claudeWorkflowEnabled);
|
|
2444
|
-
// ── Artifact delivery (terminal-agnostic) ──
|
|
2445
|
-
// `im_send_file` is the single "hand a file to the user" verb. Whatever the
|
|
2446
|
-
// caller wired as `mcpSendFile` (an IM channel push, or nothing for a
|
|
2447
|
-
// dashboard / headless turn) is wrapped so every delivery ALSO records the
|
|
2448
|
-
// file into the session's delivered-artifact manifest — the durable SSOT
|
|
2449
|
-
// that survives reload + workspace cleanup and is servable over HTTP — and
|
|
2450
|
-
// surfaces it live in the running turn's snapshot for any watching
|
|
2451
|
-
// dashboard. Always provided, so the tool + delivery prompt light up even
|
|
2452
|
-
// when there is no IM channel (the dashboard is the delivery surface then).
|
|
2453
2017
|
const deliverySessionKey = ('key' in cs && typeof cs.key === 'string') ? cs.key : null;
|
|
2454
2018
|
const wrappedSendFile = this.buildArtifactSendFile(cs.agent, deliverySessionKey, cs, mcpSendFile);
|
|
2455
|
-
const mcpSystemPrompt = appendExtraPrompt(appendExtraPrompt(appendExtraPrompt(
|
|
2456
|
-
// Always-on: `wrappedSendFile` is provided for every turn (see above),
|
|
2457
|
-
// so artifact delivery is available regardless of terminal.
|
|
2458
|
-
buildMcpDeliveryPrompt(), onInteraction && cs.agent === 'claude' ? buildClaudeAskUserPrompt() : ''), buildBrowserAutomationPrompt(browserEnabled)), workflowEnabled ? buildWorkflowOptInPrompt() : '');
|
|
2459
|
-
// mcpSystemPrompt carries behaviour directives (use im_ask_user instead of
|
|
2460
|
-
// built-in AskUserQuestion, browser automation status, artifact delivery)
|
|
2461
|
-
// that must apply on every turn, not just the first — on resume the CLI
|
|
2462
|
-
// does not automatically re-inject the previous --append-system-prompt
|
|
2463
|
-
// contents, so Claude silently regresses to the built-in tools on turn 2+.
|
|
2464
|
-
// The caller-supplied `systemPrompt` (per-task scaffolding) remains
|
|
2465
|
-
// first-turn-only since later turns inherit it via the session transcript.
|
|
2019
|
+
const mcpSystemPrompt = appendExtraPrompt(appendExtraPrompt(appendExtraPrompt(buildMcpDeliveryPrompt(), onInteraction && cs.agent === 'claude' ? buildClaudeAskUserPrompt() : ''), buildBrowserAutomationPrompt(browserEnabled)), workflowEnabled ? buildWorkflowOptInPrompt() : '');
|
|
2466
2020
|
const effectiveSystemPrompt = isFirstTurnOfSession
|
|
2467
2021
|
? appendExtraPrompt(systemPrompt, mcpSystemPrompt)
|
|
2468
2022
|
: (mcpSystemPrompt || undefined);
|
|
@@ -2485,45 +2039,31 @@ export class Bot {
|
|
|
2485
2039
|
thinkingEffort: resolvedThinkingEffort, onText,
|
|
2486
2040
|
onSessionId: syncNativeSessionId,
|
|
2487
2041
|
attachments: attachments.length ? attachments : undefined,
|
|
2488
|
-
// codex-specific
|
|
2489
2042
|
codexModel: cs.agent === 'codex' ? resolvedModel : this.codexModel,
|
|
2490
2043
|
codexFullAccess: this.codexFullAccess,
|
|
2491
2044
|
codexDeveloperInstructions: effectiveSystemPrompt || undefined,
|
|
2492
2045
|
codexExtraArgs: this.codexExtraArgs.length ? this.codexExtraArgs : undefined,
|
|
2493
2046
|
codexPrevCumulative: cs.codexCumulative,
|
|
2494
|
-
// claude-specific
|
|
2495
2047
|
claudeModel: cs.agent === 'claude' ? resolvedModel : this.claudeModel,
|
|
2496
2048
|
claudePermissionMode: this.claudePermissionMode,
|
|
2497
2049
|
claudeWorkflowEnabled: workflowEnabled,
|
|
2498
|
-
// Resolved per-stream so a live access-mode switch applies to new turns
|
|
2499
|
-
// while in-flight streams keep the mode they spawned with.
|
|
2500
2050
|
claudeAccessMode: cs.agent === 'claude' ? this.claudeAccessMode : undefined,
|
|
2501
2051
|
claudeAppendSystemPrompt: effectiveSystemPrompt || undefined,
|
|
2502
2052
|
claudeExtraArgs: this.claudeExtraArgs.length ? this.claudeExtraArgs : undefined,
|
|
2503
|
-
// gemini-specific
|
|
2504
2053
|
geminiModel: cs.agent === 'gemini' ? resolvedModel : (this.agentConfigs.gemini?.model || ''),
|
|
2505
2054
|
geminiApprovalMode: this.geminiApprovalMode,
|
|
2506
2055
|
geminiSandbox: this.geminiSandbox,
|
|
2507
2056
|
geminiSystemInstruction: effectiveSystemPrompt || undefined,
|
|
2508
2057
|
geminiExtraArgs: this.geminiExtraArgs.length ? this.geminiExtraArgs : undefined,
|
|
2509
|
-
// hermes-specific. Wire the chat's current model so /models switching in
|
|
2510
|
-
// IM takes effect even without a BYOK Profile (the BYOK injector in
|
|
2511
|
-
// stream.ts overrides this with the ACP-encoded `provider:model` when
|
|
2512
|
-
// a Profile is bound).
|
|
2513
2058
|
hermesModel: cs.agent === 'hermes' && resolvedModel ? resolvedModel : undefined,
|
|
2514
|
-
// MCP bridge
|
|
2515
2059
|
mcpSendFile: wrappedSendFile,
|
|
2516
2060
|
abortSignal,
|
|
2517
2061
|
onInteraction,
|
|
2518
2062
|
onSteerReady,
|
|
2519
2063
|
onCodexTurnReady,
|
|
2520
|
-
// Fork lineage — when set, the driver branches off the parent session.
|
|
2521
2064
|
forkOf: extras?.forkOf,
|
|
2522
2065
|
};
|
|
2523
2066
|
const result = await doStream(opts);
|
|
2524
|
-
// 'ultra' is a display-only rung (max reasoning + Workflow). The command ran on
|
|
2525
|
-
// concrete max+workflow; relabel the result effort so the reply footer (IM
|
|
2526
|
-
// final card, etc.) matches the picker. Display-only — never re-fed into a run.
|
|
2527
2067
|
if (cs.agent === 'claude' && workflowEnabled && result.thinkingEffort) {
|
|
2528
2068
|
result.thinkingEffort = 'ultra';
|
|
2529
2069
|
}
|
|
@@ -2556,11 +2096,6 @@ export class Bot {
|
|
|
2556
2096
|
return;
|
|
2557
2097
|
const bin = whichSync('caffeinate');
|
|
2558
2098
|
if (bin) {
|
|
2559
|
-
// `-dis` = prevent display sleep + idle sleep + system sleep. The `-d`
|
|
2560
|
-
// (display) flag is intentional: the agent uses macOS `screencapture`
|
|
2561
|
-
// for desktop screenshots, which returns a black frame once the
|
|
2562
|
-
// display sleeps. Users who would rather let the screen turn off
|
|
2563
|
-
// should drop brightness or close the lid against an external display.
|
|
2564
2099
|
this.keepAliveProc = spawn('caffeinate', ['-dis'], { stdio: 'ignore', detached: true });
|
|
2565
2100
|
this.keepAliveProc.unref();
|
|
2566
2101
|
this.log(`keep-alive: caffeinate (PID ${this.keepAliveProc.pid})`);
|