pikiloom 0.4.25 → 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-B8Ai-_zs.js → AgentTab-DIP-y4dl.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BCfvhkiI.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-CklI2Lh4.js → DirBrowser-Dot7ua9-.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BYLt52eV.js → ExtensionsTab-GioMx4S6.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-BlwB4Hje.js → IMAccessTab-C9_r8jJP.js} +1 -1
- package/dashboard/dist/assets/{Modal-DoamIusy.js → Modal-M1AP1sgR.js} +1 -1
- package/dashboard/dist/assets/{Modals-BGRFYHLW.js → Modals-CCm3UUfJ.js} +1 -1
- package/dashboard/dist/assets/{Select-DCGLDJPj.js → Select-YubDM16P.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-Cnv9o96n.js +1 -0
- 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-DsGKwuTP.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 +90 -192
- 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-YR4dc2lx.js +0 -1
- package/dashboard/dist/assets/SessionPanel-DZziN6ft.js +0 -1
- package/dashboard/dist/assets/SystemTab-DaJk9s8R.js +0 -1
- package/dashboard/dist/assets/index-ClrAJ5vv.js +0 -23
- package/dashboard/dist/assets/index-CoBaePpK.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,17 +530,15 @@ 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);
|
|
555
536
|
for (const filePath of files) {
|
|
556
|
-
const
|
|
557
|
-
if (!fs.existsSync(
|
|
537
|
+
const resolved = path.resolve(filePath);
|
|
538
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile())
|
|
558
539
|
continue;
|
|
559
|
-
|
|
540
|
+
const sourcePath = fs.realpathSync(resolved);
|
|
541
|
+
const relPath = path.relative(realWorkspace, sourcePath);
|
|
560
542
|
if (relPath && !relPath.startsWith('..') && !path.isAbsolute(relPath)) {
|
|
561
543
|
imported.push(relPath.split(path.sep).join(path.posix.sep));
|
|
562
544
|
continue;
|
|
@@ -567,9 +549,6 @@ export function importFilesIntoWorkspace(workspacePath, files) {
|
|
|
567
549
|
}
|
|
568
550
|
return dedupeStrings(imported);
|
|
569
551
|
}
|
|
570
|
-
// ---------------------------------------------------------------------------
|
|
571
|
-
// Ensure session workspace
|
|
572
|
-
// ---------------------------------------------------------------------------
|
|
573
552
|
export function ensureSessionWorkspace(opts) {
|
|
574
553
|
const workdir = path.resolve(opts.workdir);
|
|
575
554
|
const index = loadSessionIndex(workdir);
|
|
@@ -596,7 +575,6 @@ export function ensureSessionWorkspace(opts) {
|
|
|
596
575
|
}
|
|
597
576
|
if (!record.threadId)
|
|
598
577
|
record.threadId = normalizeThreadId(opts.threadId) || legacyThreadId(record.agent, record.sessionId);
|
|
599
|
-
// Backfill handoverFrom on first staging only — never overwrite an existing one.
|
|
600
578
|
if (!record.handoverFrom)
|
|
601
579
|
record.handoverFrom = normalizeHandoverRef(opts.handoverFrom);
|
|
602
580
|
if (!record.title && opts.title)
|
|
@@ -605,14 +583,7 @@ export function ensureSessionWorkspace(opts) {
|
|
|
605
583
|
saveSessionRecord(workdir, record);
|
|
606
584
|
return { sessionId: record.sessionId, workspacePath: record.workspacePath, record };
|
|
607
585
|
}
|
|
608
|
-
// ---------------------------------------------------------------------------
|
|
609
|
-
// Record to SessionInfo
|
|
610
|
-
// ---------------------------------------------------------------------------
|
|
611
586
|
export function managedRecordToSessionInfo(record) {
|
|
612
|
-
// Collapse pre-fix records that stored the canonical skill expansion as the
|
|
613
|
-
// title / lastQuestion / lastMessageText. New records get collapsed at write
|
|
614
|
-
// time in `prepareStreamOpts`; this read-time pass keeps existing sessions
|
|
615
|
-
// from showing the long instruction in the sidebar after the fix lands.
|
|
616
587
|
const title = collapseSkillPrompt(record.title) ?? record.title;
|
|
617
588
|
const lastQuestion = collapseSkillPrompt(record.lastQuestion) ?? record.lastQuestion;
|
|
618
589
|
const lastMessageText = collapseSkillPrompt(record.lastMessageText) ?? record.lastMessageText;
|
|
@@ -646,10 +617,6 @@ export function managedRecordToSessionInfo(record) {
|
|
|
646
617
|
handoverFrom: record.handoverFrom ?? null,
|
|
647
618
|
};
|
|
648
619
|
}
|
|
649
|
-
// ---------------------------------------------------------------------------
|
|
650
|
-
// Public session queries
|
|
651
|
-
// ---------------------------------------------------------------------------
|
|
652
|
-
// Exported for drivers
|
|
653
620
|
export function listPikiloomSessions(workdir, agent, limit) {
|
|
654
621
|
const records = sortByUpdatedAtDesc(loadSessionIndex(path.resolve(workdir)).sessions.filter(entry => entry.agent === agent));
|
|
655
622
|
return typeof limit === 'number' ? records.slice(0, limit) : records;
|
|
@@ -657,22 +624,6 @@ export function listPikiloomSessions(workdir, agent, limit) {
|
|
|
657
624
|
export function findPikiloomSession(workdir, agent, sessionId) {
|
|
658
625
|
return listPikiloomSessions(workdir, agent).find(entry => entry.sessionId === sessionId) || null;
|
|
659
626
|
}
|
|
660
|
-
/**
|
|
661
|
-
* Delete a pikiloom-managed session. Two scopes:
|
|
662
|
-
* - default: drop the index entry + recursively delete the per-session dir
|
|
663
|
-
* under `<workdir>/.pikiloom/sessions/<agent>/<sessionId>/` (and the legacy
|
|
664
|
-
* `workspaces/` path). Native agent transcript is left in place so the
|
|
665
|
-
* user can still resume the conversation outside pikiloom.
|
|
666
|
-
* - `purgeNative: true`: also call the driver's `deleteNativeSession` to
|
|
667
|
-
* remove the underlying jsonl/rollout file.
|
|
668
|
-
*
|
|
669
|
-
* Refuses to delete a session whose record is currently marked running and
|
|
670
|
-
* not stale (active process or recent mtime) — caller should stop the
|
|
671
|
-
* stream first.
|
|
672
|
-
*
|
|
673
|
-
* Sessions that exist only in the agent's native store (no pikiloom record)
|
|
674
|
-
* are still purgeable when `purgeNative` is set.
|
|
675
|
-
*/
|
|
676
627
|
export async function deleteAgentSession(opts) {
|
|
677
628
|
const resolvedWorkdir = path.resolve(opts.workdir);
|
|
678
629
|
const { agent, sessionId } = opts;
|
|
@@ -692,7 +643,8 @@ export async function deleteAgentSession(opts) {
|
|
|
692
643
|
}
|
|
693
644
|
if (record) {
|
|
694
645
|
index.sessions.splice(recordIdx, 1);
|
|
695
|
-
|
|
646
|
+
const prunedPromotions = prunePromotionsForDeletedSession(index.promotions, agent, sessionId);
|
|
647
|
+
writeSessionIndex(resolvedWorkdir, index.sessions, prunedPromotions ?? undefined);
|
|
696
648
|
result.recordRemoved = true;
|
|
697
649
|
}
|
|
698
650
|
for (const dir of [sessionDirPath(resolvedWorkdir, agent, sessionId), legacySessionWorkspacePath(resolvedWorkdir, agent, sessionId)]) {
|
|
@@ -721,11 +673,6 @@ export async function deleteAgentSession(opts) {
|
|
|
721
673
|
result.ok = true;
|
|
722
674
|
return result;
|
|
723
675
|
}
|
|
724
|
-
/**
|
|
725
|
-
* Look up the persisted model, thinkingEffort, and bound profileId for an
|
|
726
|
-
* existing session. Returns null values when the session is not found or
|
|
727
|
-
* fields are not set.
|
|
728
|
-
*/
|
|
729
676
|
export function getSessionStoredConfig(workdir, agent, sessionId) {
|
|
730
677
|
const record = findPikiloomSession(workdir, agent, sessionId);
|
|
731
678
|
return {
|
|
@@ -772,7 +719,6 @@ export function stageSessionFiles(opts) {
|
|
|
772
719
|
const importedFiles = importFilesIntoWorkspace(session.workspacePath, opts.files);
|
|
773
720
|
if (importedFiles.length) {
|
|
774
721
|
session.record.stagedFiles = dedupeStrings([...session.record.stagedFiles, ...importedFiles]);
|
|
775
|
-
/* title will be set when the first text prompt arrives */
|
|
776
722
|
saveSessionRecord(opts.workdir, session.record);
|
|
777
723
|
}
|
|
778
724
|
return {
|
|
@@ -783,9 +729,6 @@ export function stageSessionFiles(opts) {
|
|
|
783
729
|
handoverFrom: session.record.handoverFrom ?? null,
|
|
784
730
|
};
|
|
785
731
|
}
|
|
786
|
-
// ---------------------------------------------------------------------------
|
|
787
|
-
// Merge managed and native sessions
|
|
788
|
-
// ---------------------------------------------------------------------------
|
|
789
732
|
function sessionTimelineAt(session) {
|
|
790
733
|
const ts = Date.parse(session.runUpdatedAt || session.createdAt || '');
|
|
791
734
|
return Number.isFinite(ts) ? ts : Number.NEGATIVE_INFINITY;
|
|
@@ -828,13 +771,6 @@ export function mergeManagedAndNativeSessions(managedSessions, nativeSessions) {
|
|
|
828
771
|
runUpdatedAt: useNativeTimeline ? (native.runUpdatedAt ?? managed.runUpdatedAt) : (managed.runUpdatedAt ?? native.runUpdatedAt),
|
|
829
772
|
title: native.title || managed.title,
|
|
830
773
|
model: native.model || managed.model,
|
|
831
|
-
// Pikiloom-owned metadata: the native session file (Claude JSONL etc.)
|
|
832
|
-
// carries none of these, so the `...native` spread would clobber them with
|
|
833
|
-
// `undefined`/`null`. The managed record (our centralized index) is the
|
|
834
|
-
// source of truth — recover each like `model` above. Without this the list
|
|
835
|
-
// silently drops the user's per-session choices: effort/Workflow fold back
|
|
836
|
-
// to the global default (per-send `ultra` → `max` after the turn) and the
|
|
837
|
-
// BYOK Profile binding is lost on resume.
|
|
838
774
|
thinkingEffort: managed.thinkingEffort ?? native.thinkingEffort ?? null,
|
|
839
775
|
workflowEnabled: managed.workflowEnabled ?? native.workflowEnabled ?? null,
|
|
840
776
|
profileId: managed.profileId ?? native.profileId ?? null,
|
|
@@ -865,9 +801,6 @@ export function mergeManagedAndNativeSessions(managedSessions, nativeSessions) {
|
|
|
865
801
|
merged.sort((a, b) => Date.parse(b.createdAt || '') - Date.parse(a.createdAt || ''));
|
|
866
802
|
return merged;
|
|
867
803
|
}
|
|
868
|
-
// ---------------------------------------------------------------------------
|
|
869
|
-
// getSessions / getSessionTail / getSessionMessages
|
|
870
|
-
// ---------------------------------------------------------------------------
|
|
871
804
|
export function getSessions(opts) {
|
|
872
805
|
const workdir = path.resolve(opts.workdir);
|
|
873
806
|
agentLog(`[sessions] request agent=${opts.agent} workdir=${workdir} limit=${opts.limit ?? 'all'}`);
|
|
@@ -882,15 +815,11 @@ export function getSessionTail(opts) {
|
|
|
882
815
|
export function getSessionMessages(opts) {
|
|
883
816
|
return getDriver(opts.agent).getSessionMessages(opts);
|
|
884
817
|
}
|
|
885
|
-
// ---------------------------------------------------------------------------
|
|
886
|
-
// Turn windowing
|
|
887
|
-
// ---------------------------------------------------------------------------
|
|
888
818
|
function normalizeTurnWindowValue(value, fallback) {
|
|
889
819
|
if (!Number.isFinite(value) || value == null)
|
|
890
820
|
return fallback;
|
|
891
821
|
return Math.max(0, Math.floor(value));
|
|
892
822
|
}
|
|
893
|
-
/** Slice messages by turn window and count total turns. Exported for drivers. */
|
|
894
823
|
export function applyTurnWindow(allMsgs, opts = {}, richMsgs) {
|
|
895
824
|
let totalTurns = 0;
|
|
896
825
|
const turnStartIndexes = [];
|
|
@@ -900,8 +829,6 @@ export function applyTurnWindow(allMsgs, opts = {}, richMsgs) {
|
|
|
900
829
|
totalTurns++;
|
|
901
830
|
}
|
|
902
831
|
}
|
|
903
|
-
// If no rich messages provided, synthesize from plain messages so the
|
|
904
|
-
// API always returns a consistent richMessages array.
|
|
905
832
|
const rich = richMsgs ?? allMsgs.map(m => ({ role: m.role, text: m.text, blocks: [{ type: 'text', content: m.text }] }));
|
|
906
833
|
if (totalTurns <= 0) {
|
|
907
834
|
return {
|
|
@@ -968,13 +895,9 @@ export function applyTurnWindow(allMsgs, opts = {}, richMsgs) {
|
|
|
968
895
|
error: null,
|
|
969
896
|
};
|
|
970
897
|
}
|
|
971
|
-
/** Filter messages to last N turns and count total turns. Exported for drivers. */
|
|
972
898
|
export function applyTurnFilter(allMsgs, lastNTurns, richMsgs) {
|
|
973
899
|
return applyTurnWindow(allMsgs, { lastNTurns }, richMsgs);
|
|
974
900
|
}
|
|
975
|
-
// ---------------------------------------------------------------------------
|
|
976
|
-
// Session classification
|
|
977
|
-
// ---------------------------------------------------------------------------
|
|
978
901
|
const PROPOSAL_PATTERNS = /方案|option[s ]?[A-C]|plan|approach|建议|recommend|alternatively|trade-?off|pros?\s+(and|&)\s+cons?|选择|比较/i;
|
|
979
902
|
const IMPLEMENTATION_PATTERNS = /已完成|committed|done|implemented|fixed|created|wrote|修复|完成|写入|提交|applied|updated|modified|refactored/i;
|
|
980
903
|
const BLOCKED_PATTERNS = /error|failed|permission denied|cannot|无法|失败|报错|blocked|timed?\s*out/i;
|
|
@@ -983,7 +906,6 @@ export function classifySession(result) {
|
|
|
983
906
|
const message = result.message || '';
|
|
984
907
|
const firstLine = message.split('\n').find(l => l.trim())?.trim() || '';
|
|
985
908
|
const summaryText = firstLine.length > 120 ? firstLine.slice(0, 117) + '...' : firstLine;
|
|
986
|
-
// 1. Structural signals from StreamResult
|
|
987
909
|
if (result.incomplete) {
|
|
988
910
|
return {
|
|
989
911
|
outcome: 'partial',
|
|
@@ -1001,7 +923,6 @@ export function classifySession(result) {
|
|
|
1001
923
|
classifiedAt: now,
|
|
1002
924
|
};
|
|
1003
925
|
}
|
|
1004
|
-
// 2. Activity signals (tool use indicates implementation)
|
|
1005
926
|
const activity = result.activity || '';
|
|
1006
927
|
if (/\b(Edit|Write|Bash)\b/.test(activity)) {
|
|
1007
928
|
return {
|
|
@@ -1011,7 +932,6 @@ export function classifySession(result) {
|
|
|
1011
932
|
classifiedAt: now,
|
|
1012
933
|
};
|
|
1013
934
|
}
|
|
1014
|
-
// 3. Content-based classification
|
|
1015
935
|
if (BLOCKED_PATTERNS.test(message.slice(0, 500))) {
|
|
1016
936
|
return {
|
|
1017
937
|
outcome: 'blocked',
|
|
@@ -1036,7 +956,6 @@ export function classifySession(result) {
|
|
|
1036
956
|
classifiedAt: now,
|
|
1037
957
|
};
|
|
1038
958
|
}
|
|
1039
|
-
// 4. Default: informational answer
|
|
1040
959
|
return {
|
|
1041
960
|
outcome: 'answer',
|
|
1042
961
|
suggestedNextAction: null,
|
|
@@ -1044,7 +963,6 @@ export function classifySession(result) {
|
|
|
1044
963
|
classifiedAt: now,
|
|
1045
964
|
};
|
|
1046
965
|
}
|
|
1047
|
-
/** Derive a default userStatus from classification outcome */
|
|
1048
966
|
export function deriveUserStatus(outcome) {
|
|
1049
967
|
switch (outcome) {
|
|
1050
968
|
case 'answer': return 'done';
|
|
@@ -1052,14 +970,8 @@ export function deriveUserStatus(outcome) {
|
|
|
1052
970
|
default: return 'review';
|
|
1053
971
|
}
|
|
1054
972
|
}
|
|
1055
|
-
// ---------------------------------------------------------------------------
|
|
1056
|
-
// Session export/import
|
|
1057
|
-
// ---------------------------------------------------------------------------
|
|
1058
973
|
export async function exportSession(opts) {
|
|
1059
974
|
try {
|
|
1060
|
-
// Rich mode so we can include image blocks in the export. The session
|
|
1061
|
-
// pipeline always returns plain messages even when rich is set; rich is
|
|
1062
|
-
// additive.
|
|
1063
975
|
const result = await getSessionMessages({ ...opts, agent: opts.agent, rich: true });
|
|
1064
976
|
if (!result.ok)
|
|
1065
977
|
return { ok: false, content: '', filename: '', error: result.error };
|
|
@@ -1070,8 +982,6 @@ export async function exportSession(opts) {
|
|
|
1070
982
|
let ext;
|
|
1071
983
|
switch (opts.format) {
|
|
1072
984
|
case 'json': {
|
|
1073
|
-
// Materialize image bytes into inline data URLs so the JSON is a
|
|
1074
|
-
// self-contained artefact (no dangling filesystem references).
|
|
1075
985
|
const { materializeImage } = await import('./images.js');
|
|
1076
986
|
const enrichedRichMessages = richMessages?.map(message => ({
|
|
1077
987
|
...message,
|
|
@@ -1114,16 +1024,9 @@ export async function exportSession(opts) {
|
|
|
1114
1024
|
return { ok: false, content: '', filename: '', error: e.message };
|
|
1115
1025
|
}
|
|
1116
1026
|
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Render an export-friendly markdown view. Each turn renders the role header,
|
|
1119
|
-
* the text body, and (for image blocks) an inlined `` ref
|
|
1120
|
-
* so the markdown is self-contained and renders correctly in any viewer
|
|
1121
|
-
* (VSCode preview, GitHub, etc.) without external file lookups.
|
|
1122
|
-
*/
|
|
1123
1027
|
async function renderMarkdownExport(agent, timestamp, messages, richMessages) {
|
|
1124
1028
|
const lines = [`# Session Export (${agent}, ${timestamp})`, ''];
|
|
1125
1029
|
const { materializeImage } = await import('./images.js');
|
|
1126
|
-
// Walk by index so we can pair messages[i] with richMessages[i] when present.
|
|
1127
1030
|
const indexed = richMessages?.length === messages.length ? richMessages : null;
|
|
1128
1031
|
const sections = [];
|
|
1129
1032
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -1189,11 +1092,6 @@ function parseMarkdownConversation(content) {
|
|
|
1189
1092
|
for (const section of sections) {
|
|
1190
1093
|
const firstLine = section.split('\n')[0].trim().toLowerCase();
|
|
1191
1094
|
const role = firstLine.includes('user') ? 'user' : 'assistant';
|
|
1192
|
-
// Strip inlined image data URLs (``) so
|
|
1193
|
-
// the imported text body stays readable. The base64 payload itself isn't
|
|
1194
|
-
// re-attached as a MessageBlock because the import API returns plain
|
|
1195
|
-
// TailMessages; downstream agents that re-process the export will see the
|
|
1196
|
-
// alt text "[image: alt]" placeholder where the markdown image stood.
|
|
1197
1095
|
const stripped = section
|
|
1198
1096
|
.split('\n')
|
|
1199
1097
|
.slice(1)
|