pikiloom 0.4.26 → 0.4.27
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-DIP-y4dl.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BCfvhkiI.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-Dot7ua9-.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-GioMx4S6.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C9_r8jJP.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-M1AP1sgR.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-CCm3UUfJ.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-YubDM16P.js} +1 -1
- package/dashboard/dist/assets/{SessionPanel-_0dLWvEb.js → SessionPanel-Cnv9o96n.js} +1 -1
- package/dashboard/dist/assets/SystemTab-VgNivIe3.js +1 -0
- package/dashboard/dist/assets/index-BY7l20bi.js +23 -0
- package/dashboard/dist/assets/index-BlqvzDUL.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C0SAInnT.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/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/agent/session.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session workspace management, metadata persistence, classification, and export/import.
|
|
3
|
-
*/
|
|
4
1
|
import crypto from 'node:crypto';
|
|
5
2
|
import fs from 'node:fs';
|
|
6
3
|
import path from 'node:path';
|
|
@@ -8,9 +5,6 @@ import { dedupeStrings, shortValue, firstNonEmptyLine, normalizeErrorMessage, no
|
|
|
8
5
|
import { getDriver } from './driver.js';
|
|
9
6
|
import { collapseSkillPrompt } from './skills.js';
|
|
10
7
|
import { SESSION_RUNNING_THRESHOLD_MS } from '../core/constants.js';
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Private helpers
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
8
|
function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); }
|
|
15
9
|
function readJsonFile(filePath, fallback) {
|
|
16
10
|
try {
|
|
@@ -38,19 +32,12 @@ function trimSessionText(value, max = 24_000) {
|
|
|
38
32
|
return text;
|
|
39
33
|
return `${text.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
|
|
40
34
|
}
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Constants
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
35
|
const PIKILOOM_DIR = '.pikiloom';
|
|
45
36
|
const PIKILOOM_SESSIONS_DIR = path.join(PIKILOOM_DIR, 'sessions');
|
|
46
37
|
const PIKILOOM_SESSION_INDEX = path.join(PIKILOOM_SESSIONS_DIR, 'index.json');
|
|
47
38
|
const PIKILOOM_LEGACY_WORKSPACES_DIR = path.join(PIKILOOM_DIR, 'workspaces');
|
|
48
39
|
const SESSION_WORKSPACE_DIR = 'workspace';
|
|
49
40
|
const SESSION_META_FILE = 'session.json';
|
|
50
|
-
// return.json and artifact constants removed — file return is now handled by MCP bridge
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Path helpers
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
41
|
function sessionIndexPath(workdir) { return path.join(workdir, PIKILOOM_SESSION_INDEX); }
|
|
55
42
|
function sessionDirPath(workdir, agent, sessionId) { return path.join(workdir, PIKILOOM_SESSIONS_DIR, agent, sessionId); }
|
|
56
43
|
function legacySessionWorkspacePath(workdir, agent, sessionId) { return path.join(workdir, PIKILOOM_LEGACY_WORKSPACES_DIR, agent, sessionId); }
|
|
@@ -61,19 +48,12 @@ function sessionRootFromWorkspacePath(workspacePath) {
|
|
|
61
48
|
}
|
|
62
49
|
function sessionMetaPath(workspacePath) { return path.join(sessionRootFromWorkspacePath(workspacePath), SESSION_META_FILE); }
|
|
63
50
|
function legacySessionMetaPath(workspacePath) { return path.join(workspacePath, PIKILOOM_DIR, SESSION_META_FILE); }
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// ID helpers
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
/** Generate a temporary session ID for new sessions before the agent assigns one. */
|
|
68
51
|
function nextPendingSessionId() { return `pending_${crypto.randomBytes(6).toString('hex')}`; }
|
|
69
52
|
function nextThreadId() { return `thread_${crypto.randomBytes(6).toString('hex')}`; }
|
|
70
53
|
function legacyThreadId(agent, sessionId) { return `legacy:${agent}:${sessionId}`; }
|
|
71
54
|
function normalizeThreadId(value) {
|
|
72
55
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
73
56
|
}
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// Run state helpers
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
57
|
function normalizeSessionRunState(rawState) {
|
|
78
58
|
const state = typeof rawState === 'string' ? rawState.trim().toLowerCase() : '';
|
|
79
59
|
if (state === 'completed' || state === 'incomplete' || state === 'running')
|
|
@@ -111,11 +91,6 @@ function incompleteRunDetail(result) {
|
|
|
111
91
|
const message = firstNonEmptyLine(result.message || '');
|
|
112
92
|
return message ? shortValue(message, 180) : 'Last run did not complete.';
|
|
113
93
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Check whether a process is still alive. Returns true when the PID exists and we can
|
|
116
|
-
* signal it, false when the process is definitively gone, and null when we cannot tell
|
|
117
|
-
* (e.g. owned by a different user — permission denied).
|
|
118
|
-
*/
|
|
119
94
|
export function isProcessAlive(pid) {
|
|
120
95
|
if (!pid || !Number.isFinite(pid) || pid <= 0)
|
|
121
96
|
return null;
|
|
@@ -131,13 +106,6 @@ export function isProcessAlive(pid) {
|
|
|
131
106
|
return null;
|
|
132
107
|
}
|
|
133
108
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Heuristic staleness check for a session record marked 'running'. Returns true when
|
|
136
|
-
* the record should be downgraded to 'incomplete' — i.e. the owning process is gone,
|
|
137
|
-
* or (if PID is missing) the last update is older than `ageThresholdMs`.
|
|
138
|
-
*
|
|
139
|
-
* Returns false if the session might still be live and should be left alone.
|
|
140
|
-
*/
|
|
141
109
|
export function isRunningSessionStale(record, ageThresholdMs) {
|
|
142
110
|
if (record.runState !== 'running')
|
|
143
111
|
return false;
|
|
@@ -149,11 +117,6 @@ export function isRunningSessionStale(record, ageThresholdMs) {
|
|
|
149
117
|
const age = record.runUpdatedAt ? Date.now() - Date.parse(record.runUpdatedAt) : Infinity;
|
|
150
118
|
return age > ageThresholdMs;
|
|
151
119
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Scan the session index for a workdir and downgrade any 'running' record whose
|
|
154
|
-
* owning process is no longer alive (or that has gone stale past `ageThresholdMs`).
|
|
155
|
-
* Returns the number of records downgraded. Safe to call at startup and periodically.
|
|
156
|
-
*/
|
|
157
120
|
export function reconcileOrphanedRunningSessions(workdir, ageThresholdMs = 30 * 60_000) {
|
|
158
121
|
const resolvedWorkdir = path.resolve(workdir);
|
|
159
122
|
const index = loadSessionIndex(resolvedWorkdir);
|
|
@@ -183,10 +146,8 @@ export function applySessionRunResult(record, result) {
|
|
|
183
146
|
else {
|
|
184
147
|
setSessionRunState(record, 'incomplete', incompleteRunDetail(result));
|
|
185
148
|
}
|
|
186
|
-
// Auto-classify the stream result
|
|
187
149
|
const classification = classifySession({ ...result, activity: result.activity ?? null });
|
|
188
150
|
record.classification = classification;
|
|
189
|
-
// Only set userStatus if not manually overridden by the user
|
|
190
151
|
if (!record.userStatus) {
|
|
191
152
|
record.userStatus = deriveUserStatus(classification.outcome);
|
|
192
153
|
}
|
|
@@ -202,7 +163,6 @@ function normalizeHandoverRef(value) {
|
|
|
202
163
|
return { agent: agent, sessionId };
|
|
203
164
|
}
|
|
204
165
|
function normalizeSessionRecord(raw, workdir) {
|
|
205
|
-
// Support both new format (sessionId) and legacy format (localSessionId + engineSessionId)
|
|
206
166
|
const sessionId = typeof raw?.sessionId === 'string' ? raw.sessionId.trim()
|
|
207
167
|
: typeof raw?.engineSessionId === 'string' && raw.engineSessionId.trim() ? raw.engineSessionId.trim()
|
|
208
168
|
: typeof raw?.localSessionId === 'string' ? raw.localSessionId.trim()
|
|
@@ -246,20 +206,7 @@ function normalizeSessionRecord(raw, workdir) {
|
|
|
246
206
|
handoverFrom: normalizeHandoverRef(raw?.handoverFrom),
|
|
247
207
|
};
|
|
248
208
|
}
|
|
249
|
-
// ---------------------------------------------------------------------------
|
|
250
|
-
// Index persistence
|
|
251
|
-
// ---------------------------------------------------------------------------
|
|
252
|
-
/**
|
|
253
|
-
* Parsed-index cache keyed by index-file identity (mtime + size). loadSessionIndex
|
|
254
|
-
* sits on the per-turn read path (getSessionStoredConfig), every dashboard session
|
|
255
|
-
* read, and is hit several times within a single save flow — each call otherwise
|
|
256
|
-
* does readFileSync + JSON.parse + a per-record normalize pass. A cache hit costs
|
|
257
|
-
* one statSync. writeSessionIndex invalidates the entry, so a write is always
|
|
258
|
-
* re-read fresh; every writer mutates records then writes, so the shared cache is
|
|
259
|
-
* never left serving a half-mutated record.
|
|
260
|
-
*/
|
|
261
209
|
const sessionIndexCache = new Map();
|
|
262
|
-
/** Sort session records newest-first, parsing each `updatedAt` only once. */
|
|
263
210
|
function sortByUpdatedAtDesc(records) {
|
|
264
211
|
const at = new Map(records.map(r => [r, Date.parse(r.updatedAt) || 0]));
|
|
265
212
|
return records.sort((a, b) => at.get(b) - at.get(a));
|
|
@@ -278,22 +225,102 @@ function loadSessionIndex(workdir) {
|
|
|
278
225
|
}
|
|
279
226
|
const parsed = readJsonFile(filePath, { version: 1, sessions: [] });
|
|
280
227
|
const sessions = Array.isArray(parsed?.sessions) ? parsed.sessions : [];
|
|
228
|
+
const promotions = {};
|
|
229
|
+
if (parsed?.promotions && typeof parsed.promotions === 'object') {
|
|
230
|
+
for (const [k, v] of Object.entries(parsed.promotions)) {
|
|
231
|
+
if (typeof k === 'string' && k && typeof v === 'string' && v && k !== v)
|
|
232
|
+
promotions[k] = v;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
281
235
|
const data = {
|
|
282
236
|
version: 1,
|
|
283
237
|
sessions: sessions
|
|
284
238
|
.map((entry) => normalizeSessionRecord(entry, workdir))
|
|
285
239
|
.filter((entry) => !!entry)
|
|
286
240
|
.filter((entry) => !isPendingSessionId(entry.sessionId) || fs.existsSync(sessionRootFromWorkspacePath(entry.workspacePath))),
|
|
241
|
+
promotions,
|
|
287
242
|
};
|
|
288
243
|
if (stat)
|
|
289
244
|
sessionIndexCache.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, data });
|
|
290
245
|
return data;
|
|
291
246
|
}
|
|
292
|
-
function writeSessionIndex(workdir, sessions) {
|
|
247
|
+
function writeSessionIndex(workdir, sessions, promotionsOverride) {
|
|
293
248
|
const filePath = sessionIndexPath(workdir);
|
|
294
|
-
|
|
249
|
+
const promotions = promotionsOverride ?? loadSessionIndex(workdir).promotions;
|
|
250
|
+
const payload = { version: 1, sessions };
|
|
251
|
+
if (promotions && Object.keys(promotions).length)
|
|
252
|
+
payload.promotions = promotions;
|
|
253
|
+
writeJsonFile(filePath, payload);
|
|
295
254
|
sessionIndexCache.delete(filePath);
|
|
296
255
|
}
|
|
256
|
+
function prunePromotionsForDeletedSession(promotions, agent, deletedId) {
|
|
257
|
+
if (!promotions)
|
|
258
|
+
return null;
|
|
259
|
+
const out = {};
|
|
260
|
+
let changed = false;
|
|
261
|
+
for (const [key, val] of Object.entries(promotions)) {
|
|
262
|
+
const ci = key.indexOf(':');
|
|
263
|
+
const kAgent = ci > 0 ? key.slice(0, ci) : '';
|
|
264
|
+
const kOld = ci > 0 ? key.slice(ci + 1) : '';
|
|
265
|
+
if (kAgent === agent && (val === deletedId || kOld === deletedId)) {
|
|
266
|
+
changed = true;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
out[key] = val;
|
|
270
|
+
}
|
|
271
|
+
return changed ? out : null;
|
|
272
|
+
}
|
|
273
|
+
export function resolveCanonicalSessionId(workdir, agent, sessionId) {
|
|
274
|
+
if (!sessionId)
|
|
275
|
+
return sessionId;
|
|
276
|
+
const resolvedWorkdir = path.resolve(workdir);
|
|
277
|
+
const promotions = loadSessionIndex(resolvedWorkdir).promotions;
|
|
278
|
+
let id = sessionId;
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
while (!seen.has(id)) {
|
|
281
|
+
seen.add(id);
|
|
282
|
+
let next = promotions?.[`${agent}:${id}`];
|
|
283
|
+
if (!next && isPendingSessionId(id)) {
|
|
284
|
+
const aliased = readSessionDirAliasTarget(resolvedWorkdir, agent, id);
|
|
285
|
+
if (aliased) {
|
|
286
|
+
recordSessionPromotion(resolvedWorkdir, agent, id, aliased);
|
|
287
|
+
next = aliased;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!next || next === id)
|
|
291
|
+
break;
|
|
292
|
+
id = next;
|
|
293
|
+
}
|
|
294
|
+
return id;
|
|
295
|
+
}
|
|
296
|
+
function readSessionDirAliasTarget(workdir, agent, sessionId) {
|
|
297
|
+
try {
|
|
298
|
+
const dir = sessionDirPath(workdir, agent, sessionId);
|
|
299
|
+
if (!fs.lstatSync(dir).isSymbolicLink())
|
|
300
|
+
return null;
|
|
301
|
+
const native = path.basename(fs.readlinkSync(dir).replace(/[/\\]+$/, ''));
|
|
302
|
+
return native && native !== sessionId && !isPendingSessionId(native) ? native : null;
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export function getSessionPromotions(workdir) {
|
|
309
|
+
return { ...loadSessionIndex(path.resolve(workdir)).promotions };
|
|
310
|
+
}
|
|
311
|
+
function recordSessionPromotion(workdir, agent, oldId, newId) {
|
|
312
|
+
if (!oldId || !newId || oldId === newId)
|
|
313
|
+
return;
|
|
314
|
+
const resolvedWorkdir = path.resolve(workdir);
|
|
315
|
+
const index = loadSessionIndex(resolvedWorkdir);
|
|
316
|
+
const promotions = { ...index.promotions };
|
|
317
|
+
for (const k of Object.keys(promotions)) {
|
|
318
|
+
if (promotions[k] === oldId && k.startsWith(`${agent}:`))
|
|
319
|
+
promotions[k] = newId;
|
|
320
|
+
}
|
|
321
|
+
promotions[`${agent}:${oldId}`] = newId;
|
|
322
|
+
writeSessionIndex(resolvedWorkdir, index.sessions, promotions);
|
|
323
|
+
}
|
|
297
324
|
function writeSessionMeta(record) {
|
|
298
325
|
writeJsonFile(sessionMetaPath(record.workspacePath), {
|
|
299
326
|
sessionId: record.sessionId, agent: record.agent, workdir: record.workdir,
|
|
@@ -317,9 +344,6 @@ function writeSessionMeta(record) {
|
|
|
317
344
|
handoverFrom: record.handoverFrom ?? null,
|
|
318
345
|
});
|
|
319
346
|
}
|
|
320
|
-
// ---------------------------------------------------------------------------
|
|
321
|
-
// File / directory helpers
|
|
322
|
-
// ---------------------------------------------------------------------------
|
|
323
347
|
function copyPath(sourcePath, targetPath) {
|
|
324
348
|
const stat = fs.statSync(sourcePath);
|
|
325
349
|
if (stat.isDirectory()) {
|
|
@@ -339,9 +363,6 @@ function createSessionDirAlias(aliasPath, targetPath) {
|
|
|
339
363
|
}
|
|
340
364
|
catch { }
|
|
341
365
|
}
|
|
342
|
-
// ---------------------------------------------------------------------------
|
|
343
|
-
// Migration
|
|
344
|
-
// ---------------------------------------------------------------------------
|
|
345
366
|
function migrateSessionLayout(workdir, record) {
|
|
346
367
|
const targetSessionDir = sessionDirPath(workdir, record.agent, record.sessionId);
|
|
347
368
|
const targetWorkspacePath = sessionWorkspacePath(workdir, record.agent, record.sessionId);
|
|
@@ -365,9 +386,6 @@ function migrateSessionLayout(workdir, record) {
|
|
|
365
386
|
record.workspacePath = path.resolve(targetWorkspacePath);
|
|
366
387
|
return record;
|
|
367
388
|
}
|
|
368
|
-
// ---------------------------------------------------------------------------
|
|
369
|
-
// Save / update
|
|
370
|
-
// ---------------------------------------------------------------------------
|
|
371
389
|
export function saveSessionRecord(workdir, record) {
|
|
372
390
|
record = migrateSessionLayout(workdir, record);
|
|
373
391
|
ensureDir(sessionDirPath(workdir, record.agent, record.sessionId));
|
|
@@ -385,10 +403,6 @@ export function saveSessionRecord(workdir, record) {
|
|
|
385
403
|
writeSessionMeta(record);
|
|
386
404
|
return record;
|
|
387
405
|
}
|
|
388
|
-
/**
|
|
389
|
-
* Update mutable session metadata (classification, userStatus, userNote, links, migration)
|
|
390
|
-
* for an existing pikiloom-managed session. Returns true if the record was found and updated.
|
|
391
|
-
*/
|
|
392
406
|
export function updateSessionMeta(workdir, agent, sessionId, patch) {
|
|
393
407
|
const resolvedWorkdir = path.resolve(workdir);
|
|
394
408
|
const index = loadSessionIndex(resolvedWorkdir);
|
|
@@ -417,10 +431,6 @@ export function updateSessionMeta(workdir, agent, sessionId, patch) {
|
|
|
417
431
|
writeSessionMeta(record);
|
|
418
432
|
return true;
|
|
419
433
|
}
|
|
420
|
-
/**
|
|
421
|
-
* Promote a pending session to a real session ID. Renames the workspace directory
|
|
422
|
-
* and updates the index. Called after the first stream returns the agent's native ID.
|
|
423
|
-
*/
|
|
424
434
|
export function promoteSessionId(workdir, agent, pendingId, nativeId) {
|
|
425
435
|
if (!isPendingSessionId(pendingId) || !nativeId.trim())
|
|
426
436
|
return;
|
|
@@ -431,12 +441,11 @@ export function promoteSessionId(workdir, agent, pendingId, nativeId) {
|
|
|
431
441
|
return;
|
|
432
442
|
const oldDir = sessionDirPath(resolvedWorkdir, agent, pendingId);
|
|
433
443
|
const newDir = sessionDirPath(resolvedWorkdir, agent, nativeId);
|
|
434
|
-
// Move workspace directory if it exists
|
|
435
444
|
if (fs.existsSync(oldDir) && !fs.existsSync(newDir)) {
|
|
436
445
|
try {
|
|
437
446
|
fs.renameSync(oldDir, newDir);
|
|
438
447
|
}
|
|
439
|
-
catch {
|
|
448
|
+
catch {
|
|
440
449
|
try {
|
|
441
450
|
fs.cpSync(oldDir, newDir, { recursive: true });
|
|
442
451
|
fs.rmSync(oldDir, { recursive: true, force: true });
|
|
@@ -450,19 +459,6 @@ export function promoteSessionId(workdir, agent, pendingId, nativeId) {
|
|
|
450
459
|
record.workspacePath = sessionWorkspacePath(resolvedWorkdir, agent, nativeId);
|
|
451
460
|
saveSessionRecord(resolvedWorkdir, record);
|
|
452
461
|
}
|
|
453
|
-
// ---------------------------------------------------------------------------
|
|
454
|
-
// Fork lineage
|
|
455
|
-
// ---------------------------------------------------------------------------
|
|
456
|
-
/**
|
|
457
|
-
* Record a fork relationship between two pikiloom-managed sessions.
|
|
458
|
-
*
|
|
459
|
-
* Sets `migratedFrom` (with kind='fork' + forkedAtTurn) on the child and
|
|
460
|
-
* appends the reverse link on the parent's `linkedSessions`. Both sides also
|
|
461
|
-
* get `migratedTo` set on the parent so the child is a discoverable twin.
|
|
462
|
-
*
|
|
463
|
-
* No-op if either record is missing — call sites are expected to ensure both
|
|
464
|
-
* managed records exist (the child is created via the fork stream completion).
|
|
465
|
-
*/
|
|
466
462
|
export function recordFork(workdir, opts) {
|
|
467
463
|
const resolvedWorkdir = path.resolve(workdir);
|
|
468
464
|
const index = loadSessionIndex(resolvedWorkdir);
|
|
@@ -488,9 +484,6 @@ export function recordFork(workdir, opts) {
|
|
|
488
484
|
writeSessionMeta(parent);
|
|
489
485
|
writeSessionMeta(child);
|
|
490
486
|
}
|
|
491
|
-
// ---------------------------------------------------------------------------
|
|
492
|
-
// Identity sync
|
|
493
|
-
// ---------------------------------------------------------------------------
|
|
494
487
|
export function syncManagedSessionIdentity(session, workdir, nativeId) {
|
|
495
488
|
const resolvedId = nativeId.trim();
|
|
496
489
|
if (!resolvedId || session.sessionId === resolvedId)
|
|
@@ -498,16 +491,9 @@ export function syncManagedSessionIdentity(session, workdir, nativeId) {
|
|
|
498
491
|
const resolvedWorkdir = path.resolve(workdir);
|
|
499
492
|
const previousId = session.sessionId;
|
|
500
493
|
if (isPendingSessionId(previousId)) {
|
|
501
|
-
// Pending → native: move the workspace dir into the native slot and
|
|
502
|
-
// remove the pending index entry (handled by promoteSessionId).
|
|
503
494
|
promoteSessionId(resolvedWorkdir, session.record.agent, previousId, resolvedId);
|
|
504
495
|
}
|
|
505
496
|
else {
|
|
506
|
-
// Native → native rotation (Claude `--resume` can rewrite the session id
|
|
507
|
-
// mid-stream). Drop the old index entry so the dashboard does not show a
|
|
508
|
-
// stale duplicate; both jsonl files stay on disk and the workspace stays
|
|
509
|
-
// under its original native id (the next saveSessionRecord will lay down
|
|
510
|
-
// a fresh dir under the new id).
|
|
511
497
|
const index = loadSessionIndex(resolvedWorkdir);
|
|
512
498
|
const filtered = index.sessions.filter(e => !(e.agent === session.record.agent && e.sessionId === previousId));
|
|
513
499
|
if (filtered.length !== index.sessions.length)
|
|
@@ -517,11 +503,9 @@ export function syncManagedSessionIdentity(session, workdir, nativeId) {
|
|
|
517
503
|
session.workspacePath = sessionWorkspacePath(resolvedWorkdir, session.record.agent, resolvedId);
|
|
518
504
|
session.record.sessionId = resolvedId;
|
|
519
505
|
session.record.workspacePath = session.workspacePath;
|
|
506
|
+
recordSessionPromotion(resolvedWorkdir, session.record.agent, previousId, resolvedId);
|
|
520
507
|
return true;
|
|
521
508
|
}
|
|
522
|
-
// ---------------------------------------------------------------------------
|
|
523
|
-
// Title / filename helpers
|
|
524
|
-
// ---------------------------------------------------------------------------
|
|
525
509
|
export function summarizePromptTitle(prompt) {
|
|
526
510
|
const raw = String(prompt || '').replace(/\r\n?/g, '\n');
|
|
527
511
|
const text = firstNonEmptyLine(raw).replace(/\s+/g, ' ').trim()
|
|
@@ -546,9 +530,6 @@ function uniqueWorkspaceFilename(workspacePath, desiredName) {
|
|
|
546
530
|
}
|
|
547
531
|
return candidate;
|
|
548
532
|
}
|
|
549
|
-
// ---------------------------------------------------------------------------
|
|
550
|
-
// Workspace file import
|
|
551
|
-
// ---------------------------------------------------------------------------
|
|
552
533
|
export function importFilesIntoWorkspace(workspacePath, files) {
|
|
553
534
|
const imported = [];
|
|
554
535
|
const realWorkspace = fs.realpathSync(workspacePath);
|
|
@@ -556,13 +537,6 @@ export function importFilesIntoWorkspace(workspacePath, files) {
|
|
|
556
537
|
const resolved = path.resolve(filePath);
|
|
557
538
|
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile())
|
|
558
539
|
continue;
|
|
559
|
-
// Resolve symlinks on the source too — realWorkspace is already realpath'd,
|
|
560
|
-
// so comparing it against a merely path.resolve'd source makes a file that
|
|
561
|
-
// ALREADY lives in the workspace look "outside" whenever a path component is
|
|
562
|
-
// a symlink (macOS /tmp → /private/tmp, or an iCloud-synced ~/Desktop). That
|
|
563
|
-
// mis-detection re-copies an already-staged attachment under a collision
|
|
564
|
-
// name, so the same image lands in the prompt TWICE and renders twice.
|
|
565
|
-
// realpath'ing both sides keeps the in-workspace check symlink-safe.
|
|
566
540
|
const sourcePath = fs.realpathSync(resolved);
|
|
567
541
|
const relPath = path.relative(realWorkspace, sourcePath);
|
|
568
542
|
if (relPath && !relPath.startsWith('..') && !path.isAbsolute(relPath)) {
|
|
@@ -575,9 +549,6 @@ export function importFilesIntoWorkspace(workspacePath, files) {
|
|
|
575
549
|
}
|
|
576
550
|
return dedupeStrings(imported);
|
|
577
551
|
}
|
|
578
|
-
// ---------------------------------------------------------------------------
|
|
579
|
-
// Ensure session workspace
|
|
580
|
-
// ---------------------------------------------------------------------------
|
|
581
552
|
export function ensureSessionWorkspace(opts) {
|
|
582
553
|
const workdir = path.resolve(opts.workdir);
|
|
583
554
|
const index = loadSessionIndex(workdir);
|
|
@@ -604,7 +575,6 @@ export function ensureSessionWorkspace(opts) {
|
|
|
604
575
|
}
|
|
605
576
|
if (!record.threadId)
|
|
606
577
|
record.threadId = normalizeThreadId(opts.threadId) || legacyThreadId(record.agent, record.sessionId);
|
|
607
|
-
// Backfill handoverFrom on first staging only — never overwrite an existing one.
|
|
608
578
|
if (!record.handoverFrom)
|
|
609
579
|
record.handoverFrom = normalizeHandoverRef(opts.handoverFrom);
|
|
610
580
|
if (!record.title && opts.title)
|
|
@@ -613,14 +583,7 @@ export function ensureSessionWorkspace(opts) {
|
|
|
613
583
|
saveSessionRecord(workdir, record);
|
|
614
584
|
return { sessionId: record.sessionId, workspacePath: record.workspacePath, record };
|
|
615
585
|
}
|
|
616
|
-
// ---------------------------------------------------------------------------
|
|
617
|
-
// Record to SessionInfo
|
|
618
|
-
// ---------------------------------------------------------------------------
|
|
619
586
|
export function managedRecordToSessionInfo(record) {
|
|
620
|
-
// Collapse pre-fix records that stored the canonical skill expansion as the
|
|
621
|
-
// title / lastQuestion / lastMessageText. New records get collapsed at write
|
|
622
|
-
// time in `prepareStreamOpts`; this read-time pass keeps existing sessions
|
|
623
|
-
// from showing the long instruction in the sidebar after the fix lands.
|
|
624
587
|
const title = collapseSkillPrompt(record.title) ?? record.title;
|
|
625
588
|
const lastQuestion = collapseSkillPrompt(record.lastQuestion) ?? record.lastQuestion;
|
|
626
589
|
const lastMessageText = collapseSkillPrompt(record.lastMessageText) ?? record.lastMessageText;
|
|
@@ -654,10 +617,6 @@ export function managedRecordToSessionInfo(record) {
|
|
|
654
617
|
handoverFrom: record.handoverFrom ?? null,
|
|
655
618
|
};
|
|
656
619
|
}
|
|
657
|
-
// ---------------------------------------------------------------------------
|
|
658
|
-
// Public session queries
|
|
659
|
-
// ---------------------------------------------------------------------------
|
|
660
|
-
// Exported for drivers
|
|
661
620
|
export function listPikiloomSessions(workdir, agent, limit) {
|
|
662
621
|
const records = sortByUpdatedAtDesc(loadSessionIndex(path.resolve(workdir)).sessions.filter(entry => entry.agent === agent));
|
|
663
622
|
return typeof limit === 'number' ? records.slice(0, limit) : records;
|
|
@@ -665,22 +624,6 @@ export function listPikiloomSessions(workdir, agent, limit) {
|
|
|
665
624
|
export function findPikiloomSession(workdir, agent, sessionId) {
|
|
666
625
|
return listPikiloomSessions(workdir, agent).find(entry => entry.sessionId === sessionId) || null;
|
|
667
626
|
}
|
|
668
|
-
/**
|
|
669
|
-
* Delete a pikiloom-managed session. Two scopes:
|
|
670
|
-
* - default: drop the index entry + recursively delete the per-session dir
|
|
671
|
-
* under `<workdir>/.pikiloom/sessions/<agent>/<sessionId>/` (and the legacy
|
|
672
|
-
* `workspaces/` path). Native agent transcript is left in place so the
|
|
673
|
-
* user can still resume the conversation outside pikiloom.
|
|
674
|
-
* - `purgeNative: true`: also call the driver's `deleteNativeSession` to
|
|
675
|
-
* remove the underlying jsonl/rollout file.
|
|
676
|
-
*
|
|
677
|
-
* Refuses to delete a session whose record is currently marked running and
|
|
678
|
-
* not stale (active process or recent mtime) — caller should stop the
|
|
679
|
-
* stream first.
|
|
680
|
-
*
|
|
681
|
-
* Sessions that exist only in the agent's native store (no pikiloom record)
|
|
682
|
-
* are still purgeable when `purgeNative` is set.
|
|
683
|
-
*/
|
|
684
627
|
export async function deleteAgentSession(opts) {
|
|
685
628
|
const resolvedWorkdir = path.resolve(opts.workdir);
|
|
686
629
|
const { agent, sessionId } = opts;
|
|
@@ -700,7 +643,8 @@ export async function deleteAgentSession(opts) {
|
|
|
700
643
|
}
|
|
701
644
|
if (record) {
|
|
702
645
|
index.sessions.splice(recordIdx, 1);
|
|
703
|
-
|
|
646
|
+
const prunedPromotions = prunePromotionsForDeletedSession(index.promotions, agent, sessionId);
|
|
647
|
+
writeSessionIndex(resolvedWorkdir, index.sessions, prunedPromotions ?? undefined);
|
|
704
648
|
result.recordRemoved = true;
|
|
705
649
|
}
|
|
706
650
|
for (const dir of [sessionDirPath(resolvedWorkdir, agent, sessionId), legacySessionWorkspacePath(resolvedWorkdir, agent, sessionId)]) {
|
|
@@ -729,11 +673,6 @@ export async function deleteAgentSession(opts) {
|
|
|
729
673
|
result.ok = true;
|
|
730
674
|
return result;
|
|
731
675
|
}
|
|
732
|
-
/**
|
|
733
|
-
* Look up the persisted model, thinkingEffort, and bound profileId for an
|
|
734
|
-
* existing session. Returns null values when the session is not found or
|
|
735
|
-
* fields are not set.
|
|
736
|
-
*/
|
|
737
676
|
export function getSessionStoredConfig(workdir, agent, sessionId) {
|
|
738
677
|
const record = findPikiloomSession(workdir, agent, sessionId);
|
|
739
678
|
return {
|
|
@@ -780,7 +719,6 @@ export function stageSessionFiles(opts) {
|
|
|
780
719
|
const importedFiles = importFilesIntoWorkspace(session.workspacePath, opts.files);
|
|
781
720
|
if (importedFiles.length) {
|
|
782
721
|
session.record.stagedFiles = dedupeStrings([...session.record.stagedFiles, ...importedFiles]);
|
|
783
|
-
/* title will be set when the first text prompt arrives */
|
|
784
722
|
saveSessionRecord(opts.workdir, session.record);
|
|
785
723
|
}
|
|
786
724
|
return {
|
|
@@ -791,9 +729,6 @@ export function stageSessionFiles(opts) {
|
|
|
791
729
|
handoverFrom: session.record.handoverFrom ?? null,
|
|
792
730
|
};
|
|
793
731
|
}
|
|
794
|
-
// ---------------------------------------------------------------------------
|
|
795
|
-
// Merge managed and native sessions
|
|
796
|
-
// ---------------------------------------------------------------------------
|
|
797
732
|
function sessionTimelineAt(session) {
|
|
798
733
|
const ts = Date.parse(session.runUpdatedAt || session.createdAt || '');
|
|
799
734
|
return Number.isFinite(ts) ? ts : Number.NEGATIVE_INFINITY;
|
|
@@ -836,13 +771,6 @@ export function mergeManagedAndNativeSessions(managedSessions, nativeSessions) {
|
|
|
836
771
|
runUpdatedAt: useNativeTimeline ? (native.runUpdatedAt ?? managed.runUpdatedAt) : (managed.runUpdatedAt ?? native.runUpdatedAt),
|
|
837
772
|
title: native.title || managed.title,
|
|
838
773
|
model: native.model || managed.model,
|
|
839
|
-
// Pikiloom-owned metadata: the native session file (Claude JSONL etc.)
|
|
840
|
-
// carries none of these, so the `...native` spread would clobber them with
|
|
841
|
-
// `undefined`/`null`. The managed record (our centralized index) is the
|
|
842
|
-
// source of truth — recover each like `model` above. Without this the list
|
|
843
|
-
// silently drops the user's per-session choices: effort/Workflow fold back
|
|
844
|
-
// to the global default (per-send `ultra` → `max` after the turn) and the
|
|
845
|
-
// BYOK Profile binding is lost on resume.
|
|
846
774
|
thinkingEffort: managed.thinkingEffort ?? native.thinkingEffort ?? null,
|
|
847
775
|
workflowEnabled: managed.workflowEnabled ?? native.workflowEnabled ?? null,
|
|
848
776
|
profileId: managed.profileId ?? native.profileId ?? null,
|
|
@@ -873,9 +801,6 @@ export function mergeManagedAndNativeSessions(managedSessions, nativeSessions) {
|
|
|
873
801
|
merged.sort((a, b) => Date.parse(b.createdAt || '') - Date.parse(a.createdAt || ''));
|
|
874
802
|
return merged;
|
|
875
803
|
}
|
|
876
|
-
// ---------------------------------------------------------------------------
|
|
877
|
-
// getSessions / getSessionTail / getSessionMessages
|
|
878
|
-
// ---------------------------------------------------------------------------
|
|
879
804
|
export function getSessions(opts) {
|
|
880
805
|
const workdir = path.resolve(opts.workdir);
|
|
881
806
|
agentLog(`[sessions] request agent=${opts.agent} workdir=${workdir} limit=${opts.limit ?? 'all'}`);
|
|
@@ -890,15 +815,11 @@ export function getSessionTail(opts) {
|
|
|
890
815
|
export function getSessionMessages(opts) {
|
|
891
816
|
return getDriver(opts.agent).getSessionMessages(opts);
|
|
892
817
|
}
|
|
893
|
-
// ---------------------------------------------------------------------------
|
|
894
|
-
// Turn windowing
|
|
895
|
-
// ---------------------------------------------------------------------------
|
|
896
818
|
function normalizeTurnWindowValue(value, fallback) {
|
|
897
819
|
if (!Number.isFinite(value) || value == null)
|
|
898
820
|
return fallback;
|
|
899
821
|
return Math.max(0, Math.floor(value));
|
|
900
822
|
}
|
|
901
|
-
/** Slice messages by turn window and count total turns. Exported for drivers. */
|
|
902
823
|
export function applyTurnWindow(allMsgs, opts = {}, richMsgs) {
|
|
903
824
|
let totalTurns = 0;
|
|
904
825
|
const turnStartIndexes = [];
|
|
@@ -908,8 +829,6 @@ export function applyTurnWindow(allMsgs, opts = {}, richMsgs) {
|
|
|
908
829
|
totalTurns++;
|
|
909
830
|
}
|
|
910
831
|
}
|
|
911
|
-
// If no rich messages provided, synthesize from plain messages so the
|
|
912
|
-
// API always returns a consistent richMessages array.
|
|
913
832
|
const rich = richMsgs ?? allMsgs.map(m => ({ role: m.role, text: m.text, blocks: [{ type: 'text', content: m.text }] }));
|
|
914
833
|
if (totalTurns <= 0) {
|
|
915
834
|
return {
|
|
@@ -976,13 +895,9 @@ export function applyTurnWindow(allMsgs, opts = {}, richMsgs) {
|
|
|
976
895
|
error: null,
|
|
977
896
|
};
|
|
978
897
|
}
|
|
979
|
-
/** Filter messages to last N turns and count total turns. Exported for drivers. */
|
|
980
898
|
export function applyTurnFilter(allMsgs, lastNTurns, richMsgs) {
|
|
981
899
|
return applyTurnWindow(allMsgs, { lastNTurns }, richMsgs);
|
|
982
900
|
}
|
|
983
|
-
// ---------------------------------------------------------------------------
|
|
984
|
-
// Session classification
|
|
985
|
-
// ---------------------------------------------------------------------------
|
|
986
901
|
const PROPOSAL_PATTERNS = /方案|option[s ]?[A-C]|plan|approach|建议|recommend|alternatively|trade-?off|pros?\s+(and|&)\s+cons?|选择|比较/i;
|
|
987
902
|
const IMPLEMENTATION_PATTERNS = /已完成|committed|done|implemented|fixed|created|wrote|修复|完成|写入|提交|applied|updated|modified|refactored/i;
|
|
988
903
|
const BLOCKED_PATTERNS = /error|failed|permission denied|cannot|无法|失败|报错|blocked|timed?\s*out/i;
|
|
@@ -991,7 +906,6 @@ export function classifySession(result) {
|
|
|
991
906
|
const message = result.message || '';
|
|
992
907
|
const firstLine = message.split('\n').find(l => l.trim())?.trim() || '';
|
|
993
908
|
const summaryText = firstLine.length > 120 ? firstLine.slice(0, 117) + '...' : firstLine;
|
|
994
|
-
// 1. Structural signals from StreamResult
|
|
995
909
|
if (result.incomplete) {
|
|
996
910
|
return {
|
|
997
911
|
outcome: 'partial',
|
|
@@ -1009,7 +923,6 @@ export function classifySession(result) {
|
|
|
1009
923
|
classifiedAt: now,
|
|
1010
924
|
};
|
|
1011
925
|
}
|
|
1012
|
-
// 2. Activity signals (tool use indicates implementation)
|
|
1013
926
|
const activity = result.activity || '';
|
|
1014
927
|
if (/\b(Edit|Write|Bash)\b/.test(activity)) {
|
|
1015
928
|
return {
|
|
@@ -1019,7 +932,6 @@ export function classifySession(result) {
|
|
|
1019
932
|
classifiedAt: now,
|
|
1020
933
|
};
|
|
1021
934
|
}
|
|
1022
|
-
// 3. Content-based classification
|
|
1023
935
|
if (BLOCKED_PATTERNS.test(message.slice(0, 500))) {
|
|
1024
936
|
return {
|
|
1025
937
|
outcome: 'blocked',
|
|
@@ -1044,7 +956,6 @@ export function classifySession(result) {
|
|
|
1044
956
|
classifiedAt: now,
|
|
1045
957
|
};
|
|
1046
958
|
}
|
|
1047
|
-
// 4. Default: informational answer
|
|
1048
959
|
return {
|
|
1049
960
|
outcome: 'answer',
|
|
1050
961
|
suggestedNextAction: null,
|
|
@@ -1052,7 +963,6 @@ export function classifySession(result) {
|
|
|
1052
963
|
classifiedAt: now,
|
|
1053
964
|
};
|
|
1054
965
|
}
|
|
1055
|
-
/** Derive a default userStatus from classification outcome */
|
|
1056
966
|
export function deriveUserStatus(outcome) {
|
|
1057
967
|
switch (outcome) {
|
|
1058
968
|
case 'answer': return 'done';
|
|
@@ -1060,14 +970,8 @@ export function deriveUserStatus(outcome) {
|
|
|
1060
970
|
default: return 'review';
|
|
1061
971
|
}
|
|
1062
972
|
}
|
|
1063
|
-
// ---------------------------------------------------------------------------
|
|
1064
|
-
// Session export/import
|
|
1065
|
-
// ---------------------------------------------------------------------------
|
|
1066
973
|
export async function exportSession(opts) {
|
|
1067
974
|
try {
|
|
1068
|
-
// Rich mode so we can include image blocks in the export. The session
|
|
1069
|
-
// pipeline always returns plain messages even when rich is set; rich is
|
|
1070
|
-
// additive.
|
|
1071
975
|
const result = await getSessionMessages({ ...opts, agent: opts.agent, rich: true });
|
|
1072
976
|
if (!result.ok)
|
|
1073
977
|
return { ok: false, content: '', filename: '', error: result.error };
|
|
@@ -1078,8 +982,6 @@ export async function exportSession(opts) {
|
|
|
1078
982
|
let ext;
|
|
1079
983
|
switch (opts.format) {
|
|
1080
984
|
case 'json': {
|
|
1081
|
-
// Materialize image bytes into inline data URLs so the JSON is a
|
|
1082
|
-
// self-contained artefact (no dangling filesystem references).
|
|
1083
985
|
const { materializeImage } = await import('./images.js');
|
|
1084
986
|
const enrichedRichMessages = richMessages?.map(message => ({
|
|
1085
987
|
...message,
|
|
@@ -1122,16 +1024,9 @@ export async function exportSession(opts) {
|
|
|
1122
1024
|
return { ok: false, content: '', filename: '', error: e.message };
|
|
1123
1025
|
}
|
|
1124
1026
|
}
|
|
1125
|
-
/**
|
|
1126
|
-
* Render an export-friendly markdown view. Each turn renders the role header,
|
|
1127
|
-
* the text body, and (for image blocks) an inlined `` ref
|
|
1128
|
-
* so the markdown is self-contained and renders correctly in any viewer
|
|
1129
|
-
* (VSCode preview, GitHub, etc.) without external file lookups.
|
|
1130
|
-
*/
|
|
1131
1027
|
async function renderMarkdownExport(agent, timestamp, messages, richMessages) {
|
|
1132
1028
|
const lines = [`# Session Export (${agent}, ${timestamp})`, ''];
|
|
1133
1029
|
const { materializeImage } = await import('./images.js');
|
|
1134
|
-
// Walk by index so we can pair messages[i] with richMessages[i] when present.
|
|
1135
1030
|
const indexed = richMessages?.length === messages.length ? richMessages : null;
|
|
1136
1031
|
const sections = [];
|
|
1137
1032
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -1197,11 +1092,6 @@ function parseMarkdownConversation(content) {
|
|
|
1197
1092
|
for (const section of sections) {
|
|
1198
1093
|
const firstLine = section.split('\n')[0].trim().toLowerCase();
|
|
1199
1094
|
const role = firstLine.includes('user') ? 'user' : 'assistant';
|
|
1200
|
-
// Strip inlined image data URLs (``) so
|
|
1201
|
-
// the imported text body stays readable. The base64 payload itself isn't
|
|
1202
|
-
// re-attached as a MessageBlock because the import API returns plain
|
|
1203
|
-
// TailMessages; downstream agents that re-process the export will see the
|
|
1204
|
-
// alt text "[image: alt]" placeholder where the markdown image stood.
|
|
1205
1095
|
const stripped = section
|
|
1206
1096
|
.split('\n')
|
|
1207
1097
|
.slice(1)
|