mixdog 0.9.55 → 0.9.59
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/package.json +3 -1
- package/scripts/_smoke_wd.mjs +7 -0
- package/scripts/agent-terminal-reap-test.mjs +127 -2
- package/scripts/compact-file-reattach-test.mjs +88 -0
- package/scripts/compact-pressure-test.mjs +45 -21
- package/scripts/compact-recall-digest-test.mjs +57 -0
- package/scripts/compact-smoke.mjs +35 -39
- package/scripts/desktop-session-bridge-test.mjs +271 -9
- package/scripts/generate-oc-icons.mjs +11 -0
- package/scripts/live-share-test.mjs +148 -0
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/max-output-recovery-test.mjs +12 -1
- package/scripts/mouse-tracking-restore-smoke.mjs +107 -0
- package/scripts/provider-admission-scheduler-test.mjs +100 -0
- package/scripts/provider-contract-test.mjs +49 -0
- package/scripts/resource-admission-test.mjs +5 -2
- package/scripts/session-ingest-compaction-smoke.mjs +38 -0
- package/scripts/steering-drain-buckets-test.mjs +15 -0
- package/scripts/submit-commandbusy-race-test.mjs +54 -3
- package/scripts/tool-smoke.mjs +2 -3
- package/scripts/tui-ambiguous-width-test.mjs +113 -0
- package/scripts/tui-runtime-load-bench.mjs +5 -0
- package/scripts/tui-transcript-perf-test.mjs +167 -0
- package/src/app.mjs +23 -0
- package/src/cli.mjs +6 -0
- package/src/lib/keychain-cjs.cjs +70 -20
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +4 -2
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +18 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +111 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +61 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +13 -4
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +43 -2
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +112 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +203 -49
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -15
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +109 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -7
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +39 -39
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +108 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -11
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +132 -24
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +91 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +14 -33
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +169 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +474 -42
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +17 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +36 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +249 -11
- package/src/runtime/channels/data/voice-runtime-manifest.json +21 -5
- package/src/runtime/channels/lib/scheduler.mjs +8 -6
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +71 -8
- package/src/runtime/channels/lib/voice-transcription.mjs +2 -2
- package/src/runtime/channels/lib/whisper-language.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +7 -0
- package/src/runtime/memory/lib/embedding-worker.mjs +20 -0
- package/src/runtime/memory/lib/query-handlers.mjs +10 -4
- package/src/runtime/memory/lib/recall-format.mjs +43 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +90 -64
- package/src/runtime/shared/err-text.mjs +4 -1
- package/src/runtime/shared/resource-admission.mjs +11 -0
- package/src/runtime/shared/schedule-model-ref.mjs +28 -0
- package/src/runtime/shared/schedule-session-run.mjs +63 -0
- package/src/runtime/shared/schedules-db.mjs +8 -3
- package/src/runtime/shared/tool-result-summary.mjs +4 -1
- package/src/runtime/shared/tool-surface.mjs +7 -7
- package/src/runtime/shared/transcript-writer.mjs +17 -0
- package/src/session-runtime/channel-config-api.mjs +11 -0
- package/src/session-runtime/config-helpers.mjs +9 -6
- package/src/session-runtime/context-status.mjs +80 -3
- package/src/session-runtime/lifecycle-api.mjs +154 -40
- package/src/session-runtime/provider-auth-api.mjs +21 -2
- package/src/session-runtime/provider-usage.mjs +4 -1
- package/src/session-runtime/resource-api.mjs +12 -11
- package/src/session-runtime/runtime-core.mjs +45 -11
- package/src/session-runtime/session-text.mjs +56 -6
- package/src/session-runtime/session-turn-api.mjs +70 -2
- package/src/session-runtime/tool-catalog.mjs +2 -1
- package/src/session-runtime/workflow-agents-api.mjs +2 -2
- package/src/standalone/agent-tool/render.mjs +5 -1
- package/src/standalone/agent-tool.mjs +63 -3
- package/src/standalone/channel-admin.mjs +19 -3
- package/src/standalone/channel-daemon.mjs +40 -0
- package/src/tui/app/resume-picker.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +19 -0
- package/src/tui/app/transcript-window.mjs +45 -8
- package/src/tui/app/use-mouse-input.mjs +101 -31
- package/src/tui/app/use-transcript-window.mjs +53 -9
- package/src/tui/components/ToolExecution.jsx +3 -4
- package/src/tui/components/TranscriptItem.jsx +3 -0
- package/src/tui/dist/index.mjs +17391 -14920
- package/src/tui/engine/context-state.mjs +10 -1
- package/src/tui/engine/live-share.mjs +390 -0
- package/src/tui/engine/queue-helpers.mjs +23 -0
- package/src/tui/engine/session-api-ext.mjs +439 -40
- package/src/tui/engine/session-api.mjs +7 -1
- package/src/tui/engine/session-flow.mjs +21 -7
- package/src/tui/engine/tool-card-results.mjs +3 -1
- package/src/tui/engine/turn.mjs +57 -4
- package/src/tui/engine.mjs +211 -7
- package/src/tui/index.jsx +2 -0
- package/src/tui/lib/voice-setup.mjs +10 -4
- package/src/tui/paste-attachments.mjs +4 -1
- package/src/vendor/statusline/src/gateway/session-routes.mjs +24 -1
- package/src/workflows/default/WORKFLOW.md +3 -0
- package/vendor/ink/build/display-width.js +14 -0
- package/vendor/ink/build/output.js +24 -3
- package/vendor/ink/build/wrap-text.d.ts +2 -0
- package/vendor/ink/build/wrap-text.js +45 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Session read + mutate CRUD extracted verbatim from manager.mjs: lookup
|
|
3
3
|
// (getSession/listSessions/findSessionByScopeKey), message clear (with
|
|
4
4
|
// clear-fork), manual compaction, status update, and metric flush.
|
|
5
|
-
import { loadSession, saveSessionAsync, setLiveSession, listStoredSessionSummaries } from '../store.mjs';
|
|
5
|
+
import { loadSession, saveSessionAsync, setLiveSession, evictLiveSession, listStoredSessionSummaries } from '../store.mjs';
|
|
6
6
|
import { estimateMessagesTokens, estimateTranscriptContextUsage } from '../context-utils.mjs';
|
|
7
7
|
import { normalizeCompactType, DEFAULT_COMPACT_TYPE, SUMMARY_PREFIX } from '../compact.mjs';
|
|
8
8
|
import { runSessionCompaction } from './compaction-runner.mjs';
|
|
@@ -125,8 +125,8 @@ export async function clearSessionMessages(sessionId, options = {}) {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
const afterMessageTokens = estimateMessagesTokens(keep);
|
|
128
|
-
const beforeTokens = estimateTranscriptContextUsage(messages, session.tools || []);
|
|
129
|
-
const afterTokens = estimateTranscriptContextUsage(keep, session.tools || []);
|
|
128
|
+
const beforeTokens = estimateTranscriptContextUsage(messages, session.tools || [], { provider: session.provider });
|
|
129
|
+
const afterTokens = estimateTranscriptContextUsage(keep, session.tools || [], { provider: session.provider });
|
|
130
130
|
const now = Date.now();
|
|
131
131
|
// --- Fork the outgoing transcript to a separate resumable session BEFORE
|
|
132
132
|
// the wipe below. Runs for every clear path (plain /clear, auto-clear,
|
|
@@ -174,7 +174,14 @@ export async function clearSessionMessages(sessionId, options = {}) {
|
|
|
174
174
|
};
|
|
175
175
|
delete fork.liveTurnMessages;
|
|
176
176
|
setLiveSession(fork);
|
|
177
|
-
void saveSessionAsync(fork).
|
|
177
|
+
void saveSessionAsync(fork).then(() => {
|
|
178
|
+
// The fork is a cold snapshot kept for /resume. Once durable on
|
|
179
|
+
// disk it must not pin a full transcript copy (image bytes
|
|
180
|
+
// included) in the same-process cache for the rest of the
|
|
181
|
+
// process lifetime — this was the largest _liveSessions leak
|
|
182
|
+
// (one whole conversation retained per clear).
|
|
183
|
+
evictLiveSession(forkId);
|
|
184
|
+
}).catch((err) => {
|
|
178
185
|
try { process.stderr.write(`[session] clear-fork save failed (sess=${forkId}): ${err?.message || err}\n`); } catch { /* best-effort */ }
|
|
179
186
|
});
|
|
180
187
|
} catch (err) {
|
|
@@ -227,15 +234,19 @@ export async function compactSessionMessages(sessionId) {
|
|
|
227
234
|
const result = await runSessionCompaction(session, {
|
|
228
235
|
mode: 'manual',
|
|
229
236
|
force: true,
|
|
230
|
-
// /
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
237
|
+
// No compactType/model override: follow the hard-locked per-session
|
|
238
|
+
// type (main = recall-fasttrack, agent = semantic). The former forced
|
|
239
|
+
// semantic + session.model here ran every manual /compact as a
|
|
240
|
+
// flagship-model summary call (27-38s measured); fasttrack is local
|
|
241
|
+
// (ingest + digest) and semantic remains only the degraded fallback
|
|
242
|
+
// inside the runner, where resolveSemanticSummaryModel downshifts the
|
|
243
|
+
// summary model.
|
|
244
|
+
// If the semantic fallback does run, filter older source history
|
|
245
|
+
// through the same pure-conversation filter as Memory ingest_session;
|
|
246
|
+
// protected system context and recent turns are still preserved
|
|
247
|
+
// separately by semantic compaction.
|
|
236
248
|
filterOldHistoryForIngest: true,
|
|
237
249
|
provider: getProvider(session.provider),
|
|
238
|
-
model: session.model,
|
|
239
250
|
sessionId,
|
|
240
251
|
signal: getSessionAbortSignal(sessionId),
|
|
241
252
|
});
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { getProvider } from '../../providers/registry.mjs';
|
|
7
7
|
import { normalizeCompactType, DEFAULT_COMPACT_TYPE } from '../compact.mjs';
|
|
8
8
|
import { collectPromptSkillsCached, buildSkillManifest, composeSystemPrompt } from '../../context/collect.mjs';
|
|
9
|
-
import { saveSession, saveSessionAsync, loadSession, setLiveSession } from '../store.mjs';
|
|
9
|
+
import { saveSession, saveSessionAsync, saveSessionAsyncDeferred, loadSession, setLiveSession, readSessionHeartbeatMtime, readSessionPresenceMtime, isSessionPresenceOwnerDead, deleteSessionPresence } from '../store.mjs';
|
|
10
|
+
import { _getRuntimeEntry } from './runtime-liveness.mjs';
|
|
10
11
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
11
12
|
import { getHiddenAgent } from '../../internal-agents.mjs';
|
|
12
13
|
import { loadConfig } from '../../config.mjs';
|
|
@@ -481,8 +482,109 @@ export function updateSessionRoute(id, route = {}) {
|
|
|
481
482
|
}
|
|
482
483
|
|
|
483
484
|
// --- resume (reload tools for a stored session) ---
|
|
484
|
-
|
|
485
|
+
// Attach-on-resume guard: resuming a session that another live process is
|
|
486
|
+
// ACTIVELY driving right now must not create a second writer on the same
|
|
487
|
+
// file — that split-brain silently freezes one side's transcript (generation
|
|
488
|
+
// ownership drops the loser's saves). Such a resume ATTACHES instead: the
|
|
489
|
+
// caller gets the live transcript flagged remoteAttached, its submits are
|
|
490
|
+
// persisted into the shared pending spool (the owner's injection poller runs
|
|
491
|
+
// them as normal user turns), and its view refreshes from disk. ONE writer,
|
|
492
|
+
// ONE transcript, every surface talking into the same conversation. Idle
|
|
493
|
+
// sessions keep the normal single-identity handoff.
|
|
494
|
+
const ACTIVE_OWNER_HB_FRESH_MS = 2 * 60 * 1000; // heartbeat freshness window
|
|
495
|
+
const PREPARED_RESUME_LIMIT = 8;
|
|
496
|
+
const _preparedResumes = new Map();
|
|
497
|
+
|
|
498
|
+
function _prepareResumeTools(session, preset) {
|
|
499
|
+
const ownerIsAgent = isAgentOwner(session);
|
|
500
|
+
const skills = ownerIsAgent ? [] : collectPromptSkillsCached(session.cwd);
|
|
501
|
+
let toolSpec = ownerIsAgent ? 'full' : (preset || session.preset || 'full');
|
|
502
|
+
const agentRuntime = getAgentRuntimeSync();
|
|
503
|
+
if (session.profileId && agentRuntime?.getProfile) {
|
|
504
|
+
try {
|
|
505
|
+
const profile = agentRuntime.getProfile(session.profileId);
|
|
506
|
+
if (!ownerIsAgent && Array.isArray(profile?.tools)) toolSpec = profile.tools;
|
|
507
|
+
} catch { /* ignore lookup failures, keep preset fallback */ }
|
|
508
|
+
}
|
|
509
|
+
let toolsForRouting = resolveSessionTools(toolSpec, skills, { ownerIsAgentSession: ownerIsAgent });
|
|
510
|
+
if (ownerIsAgent) {
|
|
511
|
+
toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.agent || null);
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
session,
|
|
515
|
+
preset,
|
|
516
|
+
toolSpec,
|
|
517
|
+
ownerIsAgent,
|
|
518
|
+
tools: finalizeSessionToolList(toolsForRouting, {
|
|
519
|
+
schemaAllowedTools: Array.isArray(session.schemaAllowedTools) ? session.schemaAllowedTools : null,
|
|
520
|
+
disallowedTools: getHiddenAgent(session.agent || null) ? ['Skill'] : null,
|
|
521
|
+
ownerIsAgent,
|
|
522
|
+
resolvedAgent: session.agent || null,
|
|
523
|
+
}),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function _rememberPreparedResume(sessionId, prepared) {
|
|
528
|
+
_preparedResumes.delete(sessionId);
|
|
529
|
+
_preparedResumes.set(sessionId, prepared);
|
|
530
|
+
while (_preparedResumes.size > PREPARED_RESUME_LIMIT) {
|
|
531
|
+
const oldest = _preparedResumes.keys().next().value;
|
|
532
|
+
if (oldest === undefined) break;
|
|
533
|
+
_preparedResumes.delete(oldest);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Desktop session-row hover/idle prefetch. loadSession validates the atomic
|
|
538
|
+
// file signature before reusing its parsed object, so a later external write
|
|
539
|
+
// naturally misses this preparation and rebuilds from the new session object.
|
|
540
|
+
export function prefetchSession(sessionId, preset = 'full') {
|
|
541
|
+
const session = loadSession(sessionId);
|
|
542
|
+
if (!session || session.closed === true) return false;
|
|
543
|
+
_rememberPreparedResume(sessionId, _prepareResumeTools(session, preset));
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function _isActivelyOwnedElsewhere(session, sessionId) {
|
|
548
|
+
// This process already owns the runtime for the id — switching back to
|
|
549
|
+
// one of our own sessions (desktop tab switch, TUI /resume) never attaches.
|
|
550
|
+
const entry = _getRuntimeEntry(sessionId);
|
|
551
|
+
if (entry && entry.closed !== true) return false;
|
|
552
|
+
// A FORCE-KILLED owner leaves its `.own` sidecar behind looking fresh.
|
|
553
|
+
// The recorded pid is authoritative: when it no longer exists, the owner
|
|
554
|
+
// is gone — clear the stale sidecar and resume with normal ownership
|
|
555
|
+
// instead of viewer-attaching into a spool nobody drains.
|
|
556
|
+
if (isSessionPresenceOwnerDead(sessionId)) {
|
|
557
|
+
deleteSessionPresence(sessionId);
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
// Heartbeats publish only while a turn is running (≤5s cadence) and the
|
|
561
|
+
// sidecar is deleted on detach/close, so freshness here means another
|
|
562
|
+
// process is mid-conversation on this session right now. Presence (`.own`)
|
|
563
|
+
// covers the idle gaps between turns: a live interactive surface keeps
|
|
564
|
+
// refreshing it (~20s) for its CURRENT session, so cross-opening an
|
|
565
|
+
// idle-but-open session still attaches as a viewer instead of splitting
|
|
566
|
+
// ownership into two writers that clobber each other's saves.
|
|
567
|
+
const lastHb = Math.max(
|
|
568
|
+
Number(readSessionHeartbeatMtime(sessionId)) || 0,
|
|
569
|
+
Number(session.lastHeartbeatAt) || 0,
|
|
570
|
+
Number(readSessionPresenceMtime(sessionId)) || 0,
|
|
571
|
+
);
|
|
572
|
+
return lastHb > 0 && Date.now() - lastHb <= ACTIVE_OWNER_HB_FRESH_MS;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Viewer self-heal probe: true when a re-resume of this session would NO
|
|
576
|
+
// LONGER attach (owner dead or every liveness signal stale). Attached
|
|
577
|
+
// surfaces poll this so a dead owner promotes the viewer instead of leaving
|
|
578
|
+
// it spooling messages to nobody. Single source of truth: the same guard
|
|
579
|
+
// the resume path uses.
|
|
580
|
+
export function isSessionOwnerGone(sessionId) {
|
|
485
581
|
const session = loadSession(sessionId);
|
|
582
|
+
if (!session) return false;
|
|
583
|
+
return !_isActivelyOwnedElsewhere(session, sessionId);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export async function resumeSession(sessionId, preset, options = {}) {
|
|
587
|
+
let session = loadSession(sessionId);
|
|
486
588
|
if (!session)
|
|
487
589
|
return null;
|
|
488
590
|
// Resuming a closed session is a resurrection attempt — refuse. The guarded
|
|
@@ -505,34 +607,40 @@ export async function resumeSession(sessionId, preset, options = {}) {
|
|
|
505
607
|
session.desktopSession = expectedDesktop;
|
|
506
608
|
}
|
|
507
609
|
if (!session.owner) session.owner = 'user';
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.agent || null);
|
|
610
|
+
if (_isActivelyOwnedElsewhere(session, sessionId)) {
|
|
611
|
+
// ATTACH (viewer mode, zero ownership): hand back the live transcript
|
|
612
|
+
// under the SAME id, flagged remoteAttached. No tool refresh, no save,
|
|
613
|
+
// no generation claim — the session file remains exclusively the
|
|
614
|
+
// owner's. session-turn-api routes this surface's submits into the
|
|
615
|
+
// shared pending spool instead of running a local turn.
|
|
616
|
+
const attached = { ...session, remoteAttached: true };
|
|
617
|
+
delete attached.liveTurnMessages;
|
|
618
|
+
delete attached.toolApprovalHook;
|
|
619
|
+
if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
|
|
620
|
+
try { process.stderr.write(`[session] attach-on-resume: ${sessionId} is live elsewhere → viewer attach\n`); } catch { /* best-effort */ }
|
|
621
|
+
}
|
|
622
|
+
return attached;
|
|
522
623
|
}
|
|
624
|
+
const oldTools = session.tools || [];
|
|
625
|
+
const cached = _preparedResumes.get(sessionId);
|
|
626
|
+
_preparedResumes.delete(sessionId);
|
|
627
|
+
const prepared = cached?.session === session && cached?.preset === preset
|
|
628
|
+
? cached
|
|
629
|
+
: _prepareResumeTools(session, preset);
|
|
523
630
|
// Keep the persisted tool mode in sync on resume (see createSession note).
|
|
524
|
-
session.toolSpec = toolSpec;
|
|
525
|
-
session.tools =
|
|
526
|
-
schemaAllowedTools: Array.isArray(session.schemaAllowedTools) ? session.schemaAllowedTools : null,
|
|
527
|
-
disallowedTools: getHiddenAgent(session.agent || null) ? ['Skill'] : null,
|
|
528
|
-
ownerIsAgent,
|
|
529
|
-
resolvedAgent: session.agent || null,
|
|
530
|
-
});
|
|
631
|
+
session.toolSpec = prepared.toolSpec;
|
|
632
|
+
session.tools = prepared.tools;
|
|
531
633
|
const newTools = session.tools;
|
|
532
634
|
const missing = oldTools.filter(t => !newTools.find(n => n.name === t.name));
|
|
533
635
|
if (missing.length) {
|
|
534
636
|
process.stderr.write(`[session] Warning: ${missing.length} tools no longer available: ${missing.map(t => t.name).join(', ')}\n`);
|
|
535
637
|
}
|
|
536
|
-
|
|
638
|
+
// The live session already owns the refreshed tools and desktop scope.
|
|
639
|
+
// Defer the structured clone + worker round-trip so opening a conversation
|
|
640
|
+
// is not blocked on persisting the same in-memory state back to disk.
|
|
641
|
+
void saveSessionAsyncDeferred(session, { expectedGeneration: session.generation })
|
|
642
|
+
.catch((err) => {
|
|
643
|
+
try { process.stderr.write(`[session] resume save failed: ${err?.message || err}\n`); } catch {}
|
|
644
|
+
});
|
|
537
645
|
return session;
|
|
538
646
|
}
|
|
@@ -50,7 +50,7 @@ export function recordStandaloneStatusTelemetry(session, result, durationMs) {
|
|
|
50
50
|
// as afterTokens. This lights up summarizeGatewayUsage's estimate-based
|
|
51
51
|
// contextUsedPct branch (provider input_tokens swing wildly / unbounded on
|
|
52
52
|
// e.g. OpenAI gpt-5.5), and lets a genuine >100% pass through.
|
|
53
|
-
const _estTokens = estimateTranscriptContextUsage(session.messages, session.tools || []);
|
|
53
|
+
const _estTokens = estimateTranscriptContextUsage(session.messages, session.tools || [], { provider: session.provider });
|
|
54
54
|
const _compactArg = { ...(result.compact && typeof result.compact === 'object' ? result.compact : {}), afterTokens: _estTokens };
|
|
55
55
|
try {
|
|
56
56
|
const summary = {
|
|
@@ -43,6 +43,7 @@ export {
|
|
|
43
43
|
markSessionAskStart,
|
|
44
44
|
markSessionStreamDelta,
|
|
45
45
|
markSessionToolCall,
|
|
46
|
+
markSessionToolOutputTail,
|
|
46
47
|
markSessionDone,
|
|
47
48
|
markSessionEmptyFinal,
|
|
48
49
|
markSessionError,
|
|
@@ -62,10 +63,17 @@ export { previewSessionTools } from './manager/tool-resolution.mjs';
|
|
|
62
63
|
export {
|
|
63
64
|
_mergePendingMessageEntries,
|
|
64
65
|
enqueuePendingMessage,
|
|
66
|
+
enqueueRemotePendingMessage,
|
|
67
|
+
drainForeignUserInjections,
|
|
65
68
|
drainPendingMessages,
|
|
66
69
|
markCompletionEntry,
|
|
67
70
|
COMPLETION_NOTIFICATION_KIND,
|
|
71
|
+
pendingMessagesSpoolPath,
|
|
68
72
|
} from './manager/pending-messages.mjs';
|
|
73
|
+
// Interactive-presence sidecar (idle-open ownership signal for the
|
|
74
|
+
// attach-on-resume guard) — resolved through the manager facade so the
|
|
75
|
+
// session-runtime turn API reaches it as mgr.publishSessionPresence.
|
|
76
|
+
export { publishSessionPresence, deleteSessionPresence } from './store.mjs';
|
|
69
77
|
export { isInternalRuntimeNotificationText as _isInternalRuntimeNotificationText } from './manager/prompt-utils.mjs';
|
|
70
78
|
|
|
71
79
|
// ── Usage-metrics surface — re-exported unchanged so loop.mjs / smoke scripts
|
|
@@ -104,7 +112,13 @@ export const _preserveBufferConfigFields = preserveBufferConfigFields;
|
|
|
104
112
|
// ── Session lifecycle / ask / crud / close / cleanup ──────────────────────
|
|
105
113
|
export { SessionClosedError } from './manager/session-errors.mjs';
|
|
106
114
|
export { setAgentRuntime } from './manager/agent-runtime-singleton.mjs';
|
|
107
|
-
export {
|
|
115
|
+
export {
|
|
116
|
+
createSession,
|
|
117
|
+
updateSessionRoute,
|
|
118
|
+
prefetchSession,
|
|
119
|
+
resumeSession,
|
|
120
|
+
isSessionOwnerGone,
|
|
121
|
+
} from './manager/session-lifecycle.mjs';
|
|
108
122
|
export { askSession, _api_call_with_interrupt } from './manager/ask-session.mjs';
|
|
109
123
|
export {
|
|
110
124
|
_sessionMessagesAdvancedBeyondCompactedOutgoing,
|
|
@@ -119,5 +133,6 @@ export {
|
|
|
119
133
|
updateSessionStatus,
|
|
120
134
|
flushSessionMetrics,
|
|
121
135
|
} from './manager/session-crud.mjs';
|
|
136
|
+
export { deleteSession } from './store.mjs';
|
|
122
137
|
export { closeSession, abortSessionTurn } from './manager/session-close.mjs';
|
|
123
138
|
export { sweepTombstones, startIdleCleanup, stopIdleCleanup } from './manager/idle-cleanup.mjs';
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
rememberCompactTelemetry,
|
|
14
14
|
emitCompactEvent,
|
|
15
15
|
compactEventType,
|
|
16
|
+
resolveSemanticSummaryModel,
|
|
16
17
|
} from './loop/compact-policy.mjs';
|
|
17
18
|
import {
|
|
18
19
|
pruneToolOutputs,
|
|
@@ -32,6 +33,25 @@ import { traceAgentCompact, messagePrefixHash } from '../agent-trace.mjs';
|
|
|
32
33
|
import { invalidateProviderRequestToolsScope } from '../../../../session-runtime/provider-request-tools.mjs';
|
|
33
34
|
import { bumpUsageMetricsEpoch } from './manager.mjs';
|
|
34
35
|
|
|
36
|
+
const RECOVERED_ERROR_MESSAGE_MAX_CHARS = 300;
|
|
37
|
+
const ANSI_ESCAPE_RE = /[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*)?\u0007|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
38
|
+
|
|
39
|
+
function compactRecoveredError(err) {
|
|
40
|
+
const rawMessage = err?.message || String(err);
|
|
41
|
+
const message = String(rawMessage)
|
|
42
|
+
.replace(ANSI_ESCAPE_RE, '')
|
|
43
|
+
.replace(/[\r\n]+/g, ' ')
|
|
44
|
+
.replace(/\s+/g, ' ')
|
|
45
|
+
.trim();
|
|
46
|
+
return {
|
|
47
|
+
// Preserve machine-readable codes exactly; only the display payload is bounded.
|
|
48
|
+
code: err?.code ?? null,
|
|
49
|
+
message: message.length > RECOVERED_ERROR_MESSAGE_MAX_CHARS
|
|
50
|
+
? `${message.slice(0, 299)}…`
|
|
51
|
+
: message,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
export async function runPreSendCompactPass(state) {
|
|
36
56
|
const {
|
|
37
57
|
provider, messages, model, requestTools, sessionRef, sessionId, cwd, opts, signal,
|
|
@@ -47,21 +67,6 @@ export async function runPreSendCompactPass(state) {
|
|
|
47
67
|
// a best-effort JSON.stringify length — close enough to the
|
|
48
68
|
// payload we hand the provider for prefix-cache analysis.
|
|
49
69
|
const beforeCount = messages.length;
|
|
50
|
-
// beforeBytes is only ever read inside the shouldCompact telemetry
|
|
51
|
-
// branches below. Computing it eagerly serialized the ENTIRE message
|
|
52
|
-
// array (Buffer.byteLength(JSON.stringify(messages))) on every loop
|
|
53
|
-
// iteration — including the common no-compact path — which grows
|
|
54
|
-
// linearly with transcript size and was a real per-iteration drag.
|
|
55
|
-
// Defer it to a memoized lazy getter so the no-compact path pays
|
|
56
|
-
// nothing and the compact path still gets an exact byte count once.
|
|
57
|
-
let _beforeBytes;
|
|
58
|
-
let _beforeBytesComputed = false;
|
|
59
|
-
const getBeforeBytes = () => {
|
|
60
|
-
if (_beforeBytesComputed) return _beforeBytes;
|
|
61
|
-
_beforeBytesComputed = true;
|
|
62
|
-
try { _beforeBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { _beforeBytes = null; }
|
|
63
|
-
return _beforeBytes;
|
|
64
|
-
};
|
|
65
70
|
const messageTokensEst = estimateMessagesTokensSafe(messages);
|
|
66
71
|
const reactivePending = reactiveOverflowRetryPending === true;
|
|
67
72
|
const pressureTokens = compactionTelemetryPressureTokens(messageTokensEst, compactPolicy, {
|
|
@@ -93,6 +98,14 @@ export async function runPreSendCompactPass(state) {
|
|
|
93
98
|
pressureTokens,
|
|
94
99
|
});
|
|
95
100
|
} else {
|
|
101
|
+
// Snapshot BEFORE mutating the live array below. Keeping this
|
|
102
|
+
// inside the compact branch preserves the no-compact fast path
|
|
103
|
+
// while ensuring compact_meta reports real savings and the true
|
|
104
|
+
// input prefix rather than the replacement transcript.
|
|
105
|
+
let beforeBytes = null;
|
|
106
|
+
let beforePrefixHash = null;
|
|
107
|
+
try { beforeBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { /* best-effort telemetry */ }
|
|
108
|
+
try { beforePrefixHash = messagePrefixHash(messages); } catch { /* best-effort telemetry */ }
|
|
96
109
|
try { await opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
97
110
|
const compactStartedAt = Date.now();
|
|
98
111
|
// Clear the one-shot reactive-overflow flag now that this
|
|
@@ -125,6 +138,7 @@ export async function runPreSendCompactPass(state) {
|
|
|
125
138
|
let semanticCompactError = null;
|
|
126
139
|
let recallFastTrackResult = null;
|
|
127
140
|
let recallFastTrackError = null;
|
|
141
|
+
let recoveredError = null;
|
|
128
142
|
try {
|
|
129
143
|
let compactInputMessages = messages;
|
|
130
144
|
if (compactPolicy.prune) {
|
|
@@ -164,12 +178,13 @@ export async function runPreSendCompactPass(state) {
|
|
|
164
178
|
semanticCompactResult = await semanticCompactMessages(
|
|
165
179
|
provider,
|
|
166
180
|
compactInputMessages,
|
|
167
|
-
model,
|
|
181
|
+
resolveSemanticSummaryModel(sessionRef, { budgetTokens: compactBudgetTokens }) || model,
|
|
168
182
|
compactBudgetTokens,
|
|
169
183
|
{
|
|
170
184
|
reserveTokens: compactPolicy.reserveTokens,
|
|
171
185
|
providerName: sessionRef.provider || provider?.name || null,
|
|
172
186
|
sessionId,
|
|
187
|
+
cwd: sessionRef?.cwd,
|
|
173
188
|
signal,
|
|
174
189
|
sendOpts: opts,
|
|
175
190
|
promptCacheKey: opts.promptCacheKey || null,
|
|
@@ -259,6 +274,7 @@ export async function runPreSendCompactPass(state) {
|
|
|
259
274
|
} catch { /* fall through to overflow escalation */ }
|
|
260
275
|
}
|
|
261
276
|
if (compacted !== undefined) {
|
|
277
|
+
recoveredError = compactRecoveredError(compactErr);
|
|
262
278
|
try {
|
|
263
279
|
process.stderr.write(
|
|
264
280
|
`[loop] compact fallback prune recovered (sess=${sessionId || 'unknown'}): ` +
|
|
@@ -317,11 +333,11 @@ export async function runPreSendCompactPass(state) {
|
|
|
317
333
|
compact_type: compactPolicy.compactType || compactPolicy.type || DEFAULT_COMPACT_TYPE,
|
|
318
334
|
prune_count: pruneCount,
|
|
319
335
|
compact_changed: false,
|
|
320
|
-
input_prefix_hash:
|
|
336
|
+
input_prefix_hash: beforePrefixHash,
|
|
321
337
|
before_count: beforeCount,
|
|
322
338
|
after_count: messages.length,
|
|
323
|
-
before_bytes:
|
|
324
|
-
after_bytes:
|
|
339
|
+
before_bytes: beforeBytes,
|
|
340
|
+
after_bytes: beforeBytes,
|
|
325
341
|
context_window: compactPolicy.contextWindow,
|
|
326
342
|
budget_tokens: compactPolicy.boundaryTokens,
|
|
327
343
|
boundary_tokens: compactPolicy.boundaryTokens,
|
|
@@ -448,10 +464,10 @@ export async function runPreSendCompactPass(state) {
|
|
|
448
464
|
compact_type: compactPolicy.compactType || compactPolicy.type || DEFAULT_COMPACT_TYPE,
|
|
449
465
|
prune_count: pruneCount,
|
|
450
466
|
compact_changed: compactChanged || summaryChanged,
|
|
451
|
-
input_prefix_hash:
|
|
467
|
+
input_prefix_hash: beforePrefixHash,
|
|
452
468
|
before_count: beforeCount,
|
|
453
469
|
after_count: messages.length,
|
|
454
|
-
before_bytes:
|
|
470
|
+
before_bytes: beforeBytes,
|
|
455
471
|
after_bytes: afterBytes,
|
|
456
472
|
context_window: compactPolicy.contextWindow,
|
|
457
473
|
budget_tokens: compactPolicy.boundaryTokens,
|
|
@@ -464,9 +480,11 @@ export async function runPreSendCompactPass(state) {
|
|
|
464
480
|
duration_ms: compactDurationMs,
|
|
465
481
|
provider: sessionRef.provider,
|
|
466
482
|
model: sessionRef.model || model,
|
|
483
|
+
recovered_error: recoveredError,
|
|
467
484
|
details: {
|
|
468
485
|
semantic: semanticCompactResult?.diagnostics || null,
|
|
469
486
|
recallFastTrack: recallFastTrackResult?.diagnostics || null,
|
|
487
|
+
recovered_error_code: recoveredError?.code ?? null,
|
|
470
488
|
},
|
|
471
489
|
});
|
|
472
490
|
emitCompactEvent(opts, {
|
|
@@ -6,6 +6,24 @@
|
|
|
6
6
|
* race protection is preserved by ordering.
|
|
7
7
|
*/
|
|
8
8
|
import { parentPort } from 'worker_threads';
|
|
9
|
+
|
|
10
|
+
// Route every stray stdout/stderr print through the parent as a `{ __log }`
|
|
11
|
+
// message instead of worker stdio. Piped worker stdio (stdout:true) keeps the
|
|
12
|
+
// parent's event loop alive for the worker's lifetime once read, and default
|
|
13
|
+
// (copied) stdio bypasses the TUI's process.stderr.write guard and prints
|
|
14
|
+
// over the terminal frame. postMessage does neither: the parent writes the
|
|
15
|
+
// text through its own guarded stderr.
|
|
16
|
+
function _forwardWrite(chunk, encoding, callback) {
|
|
17
|
+
try { parentPort.postMessage({ __log: typeof chunk === 'string' ? chunk : String(chunk) }); } catch { /* best-effort */ }
|
|
18
|
+
const cb = typeof encoding === 'function' ? encoding : callback;
|
|
19
|
+
if (typeof cb === 'function') cb();
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
process.stdout.write = _forwardWrite;
|
|
24
|
+
process.stderr.write = _forwardWrite;
|
|
25
|
+
} catch { /* best-effort: worker still functions with default stdio */ }
|
|
26
|
+
|
|
9
27
|
import { _saveSessionSync } from './store.mjs';
|
|
10
28
|
|
|
11
29
|
parentPort.on('message', ({ session, opts, reqId }) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, unlinkSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
2
2
|
import * as fsp from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { getPluginData } from '../../config.mjs';
|
|
@@ -50,3 +50,93 @@ export function deleteHeartbeat(id) {
|
|
|
50
50
|
try { unlinkSync(_heartbeatPath(id)); } catch { /* ignore */ }
|
|
51
51
|
_hbLastAt.delete(id);
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
// Batch reader for session catalogs. Scanning only the handful of `.hb`
|
|
55
|
+
// sidecars avoids one failed stat per historical session while still exposing
|
|
56
|
+
// cross-process turn activity to desktop/TUI resume lists.
|
|
57
|
+
export function listSessionHeartbeatMtimes() {
|
|
58
|
+
const result = new Map();
|
|
59
|
+
try {
|
|
60
|
+
const dir = getStoreDir();
|
|
61
|
+
for (const filename of readdirSync(dir)) {
|
|
62
|
+
if (!filename.endsWith('.hb')) continue;
|
|
63
|
+
const id = filename.slice(0, -3);
|
|
64
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id)) continue;
|
|
65
|
+
try {
|
|
66
|
+
const mtime = statSync(join(dir, filename)).mtimeMs || 0;
|
|
67
|
+
if (mtime > 0) result.set(id, mtime);
|
|
68
|
+
} catch { /* sidecar disappeared after readdir */ }
|
|
69
|
+
}
|
|
70
|
+
} catch { /* catalog activity is best-effort */ }
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Interactive presence publish ──────────────────────────
|
|
75
|
+
// `<id>.own` marks a live interactive surface (TUI/desktop engine) HOLDING
|
|
76
|
+
// the session open — including idle time between turns. Kept separate from
|
|
77
|
+
// `.hb` (turn-activity signal consumed by the status aggregator and deleted
|
|
78
|
+
// at turn end) so idle presence never fakes a "running" badge. Consumed by
|
|
79
|
+
// the attach-on-resume guard (_isActivelyOwnedElsewhere): fresh presence
|
|
80
|
+
// means a cross-open must attach as a viewer instead of claiming a second
|
|
81
|
+
// writer on the same session file (dual-writer last-save-wins clobbering).
|
|
82
|
+
// Refreshed by the engine share tick (~3s call, throttled here) and cleared
|
|
83
|
+
// on session switch/dispose; staleness covers crashes.
|
|
84
|
+
const _PRESENCE_THROTTLE_MS = 20_000;
|
|
85
|
+
const _presenceLastAt = new Map();
|
|
86
|
+
|
|
87
|
+
function _presencePath(id) {
|
|
88
|
+
if (!id || typeof id !== 'string' || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
89
|
+
throw new Error(`[session-store] invalid session id: ${JSON.stringify(id)}`);
|
|
90
|
+
}
|
|
91
|
+
return join(getStoreDir(), `${id}.own`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function publishSessionPresence(id, ts) {
|
|
95
|
+
if (!id) return;
|
|
96
|
+
const now = ts || Date.now();
|
|
97
|
+
const last = _presenceLastAt.get(id) || 0;
|
|
98
|
+
if (now - last < _PRESENCE_THROTTLE_MS) return;
|
|
99
|
+
const target = _presencePath(id);
|
|
100
|
+
_presenceLastAt.set(id, now);
|
|
101
|
+
void fsp.writeFile(target, `${now}\n${process.pid}\n`, 'utf8').catch(() => {});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function deleteSessionPresence(id) {
|
|
105
|
+
if (!id) return;
|
|
106
|
+
try { unlinkSync(_presencePath(id)); } catch { /* ignore */ }
|
|
107
|
+
_presenceLastAt.delete(id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function readSessionPresenceMtime(id) {
|
|
111
|
+
if (!id) return 0;
|
|
112
|
+
try {
|
|
113
|
+
const path = _presencePath(id);
|
|
114
|
+
return existsSync(path) ? (statSync(path).mtimeMs || 0) : 0;
|
|
115
|
+
} catch {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A force-killed owner never clears its presence sidecar, so bare mtime
|
|
121
|
+
// freshness would lock every resume inside the window into viewer mode with
|
|
122
|
+
// nobody draining the shared spool. The `.own` payload records the holder's
|
|
123
|
+
// pid; a recorded-but-dead pid is authoritative proof the owner is gone.
|
|
124
|
+
export function isSessionPresenceOwnerDead(id) {
|
|
125
|
+
if (!id) return false;
|
|
126
|
+
let pid = 0;
|
|
127
|
+
try {
|
|
128
|
+
const path = _presencePath(id);
|
|
129
|
+
if (!existsSync(path)) return false;
|
|
130
|
+
pid = Number(String(readFileSync(path, 'utf8')).split('\n')[1]) || 0;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (!pid) return false;
|
|
135
|
+
try {
|
|
136
|
+
process.kill(pid, 0);
|
|
137
|
+
return false;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// ESRCH: no such process. EPERM means the pid exists (alive).
|
|
140
|
+
return error?.code === 'ESRCH';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -9,8 +9,13 @@ import { mkdirSync } from 'fs';
|
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { getPluginData } from '../config.mjs';
|
|
11
11
|
import { updateJsonAtomicSync, updateJsonAtomic, writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
12
|
+
import {
|
|
13
|
+
cleanSessionPreview,
|
|
14
|
+
isSessionPreviewNoise,
|
|
15
|
+
sessionMessageText,
|
|
16
|
+
} from '../../../../session-runtime/session-text.mjs';
|
|
12
17
|
|
|
13
|
-
export const SESSION_SUMMARY_INDEX_VERSION =
|
|
18
|
+
export const SESSION_SUMMARY_INDEX_VERSION = 2;
|
|
14
19
|
|
|
15
20
|
export function summaryIndexPath() {
|
|
16
21
|
const dir = getPluginData();
|
|
@@ -19,52 +24,28 @@ export function summaryIndexPath() {
|
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
function _messageText(content) {
|
|
22
|
-
|
|
23
|
-
if (Array.isArray(content)) {
|
|
24
|
-
return content.map((part) => {
|
|
25
|
-
if (typeof part === 'string') return part;
|
|
26
|
-
if (part?.type === 'text') return part.text || '';
|
|
27
|
-
if (typeof part?.text === 'string') return part.text;
|
|
28
|
-
try { return JSON.stringify(part); } catch { return ''; }
|
|
29
|
-
}).filter(Boolean).join('\n');
|
|
30
|
-
}
|
|
31
|
-
if (content && typeof content === 'object') {
|
|
32
|
-
if (typeof content.text === 'string') return content.text;
|
|
33
|
-
try { return JSON.stringify(content); } catch { return ''; }
|
|
34
|
-
}
|
|
35
|
-
return '';
|
|
27
|
+
return sessionMessageText(content);
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
export function _cleanPreview(text, max = 240) {
|
|
39
|
-
const value =
|
|
31
|
+
const value = cleanSessionPreview(text, max);
|
|
40
32
|
return value.length > max ? value.slice(0, max).replace(/\s+\S*$/, '').trim() : value;
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
function _isPreviewNoise(text) {
|
|
44
|
-
|
|
45
|
-
if (!value) return true;
|
|
46
|
-
if (value.startsWith('<system-reminder>')) return true;
|
|
47
|
-
if (/^Reference files:/i.test(value)) return true;
|
|
48
|
-
return false;
|
|
36
|
+
return isSessionPreviewNoise(text);
|
|
49
37
|
}
|
|
50
38
|
|
|
51
39
|
function _sessionPreview(session) {
|
|
52
40
|
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
53
|
-
let first = '';
|
|
54
41
|
for (const m of messages) {
|
|
55
42
|
if (m?.role !== 'user') continue;
|
|
56
|
-
const
|
|
57
|
-
if (_isPreviewNoise(
|
|
58
|
-
|
|
43
|
+
const raw = _messageText(m.content);
|
|
44
|
+
if (_isPreviewNoise(raw)) continue;
|
|
45
|
+
const text = _cleanPreview(raw);
|
|
46
|
+
if (text) return text;
|
|
59
47
|
}
|
|
60
|
-
|
|
61
|
-
const m = messages[i];
|
|
62
|
-
if (m?.role !== 'user') continue;
|
|
63
|
-
const text = _cleanPreview(_messageText(m.content));
|
|
64
|
-
if (_isPreviewNoise(text)) continue;
|
|
65
|
-
return text || first;
|
|
66
|
-
}
|
|
67
|
-
return first;
|
|
48
|
+
return '';
|
|
68
49
|
}
|
|
69
50
|
|
|
70
51
|
function _sessionMessageCount(session) {
|