groove-dev 0.27.185 → 0.27.186
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +1 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +83 -42
- package/node_modules/@groove-dev/daemon/src/rotator.js +6 -1
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +59 -0
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +22 -4
- package/node_modules/@groove-dev/gui/dist/assets/{index-DEZwM4Hb.js → index-DOOaCFRS.js} +60 -60
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +36 -4
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +25 -0
- package/node_modules/@groove-dev/gui/src/lib/logpaths.js +45 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +11 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +1 -0
- package/packages/daemon/src/journalist.js +83 -42
- package/packages/daemon/src/rotator.js +6 -1
- package/packages/gui/dist/assets/{index-DEZwM4Hb.js → index-DOOaCFRS.js} +60 -60
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +36 -4
- package/packages/gui/src/components/editor/terminal.jsx +25 -0
- package/packages/gui/src/lib/logpaths.js +45 -0
- package/packages/gui/src/stores/slices/ui-slice.js +11 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DOOaCFRS.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BYKpdS2W.js">
|
|
@@ -12,6 +12,7 @@ import { useGrooveStore } from '../../stores/groove';
|
|
|
12
12
|
import { cn } from '../../lib/cn';
|
|
13
13
|
import { timeAgo } from '../../lib/format';
|
|
14
14
|
import { api } from '../../lib/api';
|
|
15
|
+
import { extractLogPaths, logLabel } from '../../lib/logpaths';
|
|
15
16
|
import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
16
17
|
import { TableTree } from '../ui/table-tree';
|
|
17
18
|
|
|
@@ -346,8 +347,14 @@ function UserMessage({ msg }) {
|
|
|
346
347
|
function InnerChatMessage({ msg, agent }) {
|
|
347
348
|
const [collapsed, setCollapsed] = useState(msg.text?.length > 400);
|
|
348
349
|
const isLong = msg.text?.length > 400;
|
|
349
|
-
const
|
|
350
|
-
|
|
350
|
+
const info = msg.innerchat || {};
|
|
351
|
+
// `peer` is the current field; `fromAgent` is the pre-`tell` schema still
|
|
352
|
+
// sitting in persisted chatHistory. Fall back so old bubbles never render
|
|
353
|
+
// "undefined".
|
|
354
|
+
const peer = info.peer || info.fromAgent;
|
|
355
|
+
const peerName = peer?.name || 'another agent';
|
|
356
|
+
const outbound = info.direction === 'out';
|
|
357
|
+
const isAnswer = info.kind === 'answer' || info.kind === 'reply';
|
|
351
358
|
const me = agent?.name || 'this agent';
|
|
352
359
|
|
|
353
360
|
return (
|
|
@@ -356,10 +363,10 @@ function InnerChatMessage({ msg, agent }) {
|
|
|
356
363
|
<ArrowLeftRight size={10} className="text-indigo flex-shrink-0" />
|
|
357
364
|
<span className="text-2xs font-semibold text-indigo font-sans">InnerChat</span>
|
|
358
365
|
<span className="text-2xs text-text-3 font-sans truncate">
|
|
359
|
-
{outbound ? `${me} → ${
|
|
366
|
+
{outbound ? `${me} → ${peerName}` : `${peerName} → ${me}`}
|
|
360
367
|
</span>
|
|
361
368
|
<span className="text-2xs text-text-4 font-sans">
|
|
362
|
-
{
|
|
369
|
+
{isAnswer ? 'answer' : outbound ? 'asked' : 'asks'}
|
|
363
370
|
</span>
|
|
364
371
|
<span className="text-[10px] text-text-4 font-sans ml-auto flex-shrink-0">{timeAgo(msg.timestamp)}</span>
|
|
365
372
|
</div>
|
|
@@ -378,6 +385,30 @@ function InnerChatMessage({ msg, agent }) {
|
|
|
378
385
|
);
|
|
379
386
|
}
|
|
380
387
|
|
|
388
|
+
// One-click "tail" chips for any log paths the agent mentioned — saves asking
|
|
389
|
+
// "what's the log file?" and hand-copying it into a terminal.
|
|
390
|
+
function LogChips({ text }) {
|
|
391
|
+
const runInTerminal = useGrooveStore((s) => s.runInTerminal);
|
|
392
|
+
const paths = useMemo(() => extractLogPaths(text), [text]);
|
|
393
|
+
if (paths.length === 0) return null;
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<div className="ml-3.5 mt-1.5 flex flex-wrap gap-1.5">
|
|
397
|
+
{paths.map((path) => (
|
|
398
|
+
<button
|
|
399
|
+
key={path}
|
|
400
|
+
onClick={() => runInTerminal(`tail -f ${path}`)}
|
|
401
|
+
title={`tail -f ${path}`}
|
|
402
|
+
className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-accent/10 hover:bg-accent/20 text-accent text-[11px] font-medium font-sans cursor-pointer transition-colors max-w-full"
|
|
403
|
+
>
|
|
404
|
+
<Terminal size={11} className="flex-shrink-0" />
|
|
405
|
+
<span className="truncate">tail {logLabel(path)}</span>
|
|
406
|
+
</button>
|
|
407
|
+
))}
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
381
412
|
function AgentMessage({ msg, agent, answeredTo }) {
|
|
382
413
|
const [collapsed, setCollapsed] = useState(msg.text?.length > 600);
|
|
383
414
|
const isLong = msg.text?.length > 600;
|
|
@@ -399,6 +430,7 @@ function AgentMessage({ msg, agent, answeredTo }) {
|
|
|
399
430
|
<div className={cn('pl-3.5 py-1 border-l', answeredTo ? 'border-indigo/50' : 'border-accent')}>
|
|
400
431
|
<StructuredMessage text={collapsed ? msg.text.slice(0, 600) + '...' : msg.text} />
|
|
401
432
|
</div>
|
|
433
|
+
<LogChips text={msg.text} />
|
|
402
434
|
{collapsed && (
|
|
403
435
|
<button
|
|
404
436
|
onClick={() => setCollapsed(false)}
|
|
@@ -31,6 +31,7 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
31
31
|
const mountedRef = useRef(false);
|
|
32
32
|
const visibleRef = useRef(visible);
|
|
33
33
|
const lastSizeRef = useRef({ cols: 0, rows: 0 });
|
|
34
|
+
const outputReadyRef = useRef(false);
|
|
34
35
|
|
|
35
36
|
useEffect(() => {
|
|
36
37
|
registerKill?.(tabId, () => {
|
|
@@ -41,6 +42,28 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
41
42
|
});
|
|
42
43
|
}, [tabId, registerKill]);
|
|
43
44
|
|
|
45
|
+
// Run a command dropped into the store by another component (e.g. a "tail
|
|
46
|
+
// log" chip in chat). Only the visible tab handles it, and we poll until this
|
|
47
|
+
// instance's PTY is ready — a freshly opened terminal spawns asynchronously,
|
|
48
|
+
// and keystrokes sent before it's ready are dropped.
|
|
49
|
+
const pendingCommand = useGrooveStore((s) => s.terminalPendingCommand);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!pendingCommand || !visible) return;
|
|
52
|
+
let tries = 0;
|
|
53
|
+
let timer = null;
|
|
54
|
+
const send = () => {
|
|
55
|
+
const ws = useGrooveStore.getState().ws;
|
|
56
|
+
if (ws?.readyState === WebSocket.OPEN && termIdRef.current && outputReadyRef.current) {
|
|
57
|
+
ws.send(JSON.stringify({ type: 'terminal:input', id: termIdRef.current, data: pendingCommand.command + '\r' }));
|
|
58
|
+
useGrooveStore.getState().clearTerminalPendingCommand();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (tries++ < 40) timer = setTimeout(send, 150); // ~6s max, covers spawn
|
|
62
|
+
};
|
|
63
|
+
send();
|
|
64
|
+
return () => clearTimeout(timer);
|
|
65
|
+
}, [pendingCommand, visible]);
|
|
66
|
+
|
|
44
67
|
useEffect(() => {
|
|
45
68
|
if (!containerRef.current || mountedRef.current) return;
|
|
46
69
|
mountedRef.current = true;
|
|
@@ -111,6 +134,7 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
111
134
|
// Wipe xterm so garbled output from wrong-sized PTY is never visible
|
|
112
135
|
term.reset();
|
|
113
136
|
outputReady = true;
|
|
137
|
+
outputReadyRef.current = true;
|
|
114
138
|
// Ask the shell to clear screen and redraw its prompt
|
|
115
139
|
const w2 = useGrooveStore.getState().ws;
|
|
116
140
|
if (w2?.readyState === WebSocket.OPEN && termIdRef.current) {
|
|
@@ -121,6 +145,7 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
121
145
|
if (outputReady) term.write(msg.data);
|
|
122
146
|
} else if (msg.type === 'terminal:exit' && msg.id === termIdRef.current) {
|
|
123
147
|
outputReady = true;
|
|
148
|
+
outputReadyRef.current = false; // no live shell to accept commands
|
|
124
149
|
term.write('\r\n\x1b[90m[session ended]\x1b[0m\r\n');
|
|
125
150
|
termIdRef.current = null;
|
|
126
151
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
// Pull tailable log targets out of an agent's message so the chat can offer a
|
|
4
|
+
// one-click "tail" instead of the user asking "what's the log file?".
|
|
5
|
+
//
|
|
6
|
+
// Two signals, in priority order:
|
|
7
|
+
// 1. An explicit `tail -f <path>` the agent already wrote — highest confidence.
|
|
8
|
+
// 2. Any path-like token ending in .log.
|
|
9
|
+
// Both are conservative: a bare word like "changelog" is not a path and won't
|
|
10
|
+
// match, and results are de-duped by full path.
|
|
11
|
+
|
|
12
|
+
const TAIL_RE = /tail\s+(?:-[a-zA-Z]+\s+)*([~/]?[\w./-]+)/g;
|
|
13
|
+
const DOTLOG_RE = /(?:^|[\s'"`(=])([~/]?(?:[\w.-]+\/)*[\w.-]+\.log)\b/g;
|
|
14
|
+
|
|
15
|
+
function clean(p) {
|
|
16
|
+
return p.replace(/^['"`]+/, '').replace(/['"`.,;:)]+$/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function extractLogPaths(text) {
|
|
20
|
+
if (!text || typeof text !== 'string') return [];
|
|
21
|
+
const out = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
|
|
24
|
+
const add = (raw) => {
|
|
25
|
+
const p = clean(raw);
|
|
26
|
+
// Require something path-shaped: a slash, a ~, or a .log extension. Guards
|
|
27
|
+
// against picking up plain words that followed "tail".
|
|
28
|
+
if (!p || p.length < 3) return;
|
|
29
|
+
if (!p.endsWith('.log') && !p.includes('/') && !p.startsWith('~')) return;
|
|
30
|
+
if (seen.has(p)) return;
|
|
31
|
+
seen.add(p);
|
|
32
|
+
out.push(p);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
for (const m of text.matchAll(TAIL_RE)) add(m[1]);
|
|
36
|
+
for (const m of text.matchAll(DOTLOG_RE)) add(m[1]);
|
|
37
|
+
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A short label for the chip — the basename, which is what the user recognizes.
|
|
42
|
+
export function logLabel(path) {
|
|
43
|
+
const base = path.replace(/\/+$/, '').split('/').pop() || path;
|
|
44
|
+
return base.length > 28 ? `…${base.slice(-27)}` : base;
|
|
45
|
+
}
|
|
@@ -106,6 +106,17 @@ export const createUiSlice = (set, get) => ({
|
|
|
106
106
|
set(updates);
|
|
107
107
|
localStorage.setItem('groove:terminalVisible', String(v));
|
|
108
108
|
},
|
|
109
|
+
|
|
110
|
+
// Cross-component channel for running a command in the terminal panel. The
|
|
111
|
+
// terminal id lives inside the active TerminalInstance, so components can't
|
|
112
|
+
// send to it directly — they drop a command here, the visible instance picks
|
|
113
|
+
// it up (waiting for its PTY to be ready) and clears it.
|
|
114
|
+
terminalPendingCommand: null,
|
|
115
|
+
runInTerminal(command) {
|
|
116
|
+
get().setTerminalVisible(true);
|
|
117
|
+
set({ terminalPendingCommand: { command, ts: Date.now() } });
|
|
118
|
+
},
|
|
119
|
+
clearTerminalPendingCommand() { set({ terminalPendingCommand: null }); },
|
|
109
120
|
setTerminalHeight(h) {
|
|
110
121
|
set({ terminalHeight: h });
|
|
111
122
|
localStorage.setItem('groove:terminalHeight', String(h));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.186",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -1441,6 +1441,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1441
1441
|
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
1442
1442
|
'onboardingDismissed', 'defaultModel', 'defaultChatProvider', 'defaultChatModel',
|
|
1443
1443
|
'dataSharingDismissed', 'replayCeiling', 'velocityCeiling', 'journalistModelTier',
|
|
1444
|
+
'resumeBudgetChars',
|
|
1444
1445
|
];
|
|
1445
1446
|
for (const key of Object.keys(req.body)) {
|
|
1446
1447
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
@@ -10,6 +10,12 @@ import { agentLogPath } from './process.js';
|
|
|
10
10
|
const DEFAULT_INTERVAL = 300_000; // 5 minutes (safety-net fallback; event-driven triggers handle the normal case)
|
|
11
11
|
const MAX_LOG_CHARS = 100_000; // ~25k tokens budget for synthesis input (captures 80-90% of recent activity)
|
|
12
12
|
const DEBOUNCE_MS = 10_000; // requestSynthesis debounce window
|
|
13
|
+
// Conversation carried across a rotation (~37K tokens at 150K chars). This is
|
|
14
|
+
// deliberately generous: a rotation is the one lossy step in an infinite
|
|
15
|
+
// session, and re-reading the recent dialogue once per rotation is far cheaper
|
|
16
|
+
// than the degradation from an agent that forgot its own history. Tune via
|
|
17
|
+
// config resumeBudgetChars (lower for cost-sensitive fleets).
|
|
18
|
+
const DEFAULT_RESUME_BUDGET_CHARS = 150_000;
|
|
13
19
|
|
|
14
20
|
export class Journalist {
|
|
15
21
|
constructor(daemon) {
|
|
@@ -1061,7 +1067,51 @@ export class Journalist {
|
|
|
1061
1067
|
*
|
|
1062
1068
|
* Budget: keeps recent turns verbatim, summarizes oldest if over maxChars.
|
|
1063
1069
|
*/
|
|
1064
|
-
extractConversationThread(agent, { maxChars
|
|
1070
|
+
extractConversationThread(agent, { maxChars } = {}) {
|
|
1071
|
+
// Budget is configurable (resumeBudgetChars): this is the "how much memory
|
|
1072
|
+
// survives a rotation" dial. Bigger costs more tokens per rotation but
|
|
1073
|
+
// preserves more of the session; a rotation is lossy exactly here.
|
|
1074
|
+
if (!maxChars) maxChars = this.daemon.config?.resumeBudgetChars ?? DEFAULT_RESUME_BUDGET_CHARS;
|
|
1075
|
+
const merged = this._parseConversationTurns(agent);
|
|
1076
|
+
if (!merged || merged.length === 0) return null;
|
|
1077
|
+
|
|
1078
|
+
// Build the thread — keep recent turns verbatim, truncate old ones if over budget
|
|
1079
|
+
let thread = '';
|
|
1080
|
+
const formatted = merged.map((t) => {
|
|
1081
|
+
const label = t.role === 'user' ? 'USER' : 'CLAUDE';
|
|
1082
|
+
return `[${label}]:\n${t.text}`;
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
// Start from the end (most recent) and work backwards to fill budget
|
|
1086
|
+
const recentFirst = [...formatted].reverse();
|
|
1087
|
+
const kept = [];
|
|
1088
|
+
let totalLen = 0;
|
|
1089
|
+
|
|
1090
|
+
for (const entry of recentFirst) {
|
|
1091
|
+
if (totalLen + entry.length > maxChars) {
|
|
1092
|
+
// Truncate this entry to fit remaining budget
|
|
1093
|
+
const remaining = maxChars - totalLen;
|
|
1094
|
+
if (remaining > 200) {
|
|
1095
|
+
kept.push(entry.slice(0, remaining) + '\n[...truncated]');
|
|
1096
|
+
}
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
kept.push(entry);
|
|
1100
|
+
totalLen += entry.length;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Reverse back to chronological order
|
|
1104
|
+
kept.reverse();
|
|
1105
|
+
thread = kept.join('\n\n---\n\n');
|
|
1106
|
+
|
|
1107
|
+
return thread;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Parse the full user↔assistant turn list from an agent's stream-json log.
|
|
1112
|
+
* Returns merged turns in chronological order, or null if no log/turns.
|
|
1113
|
+
*/
|
|
1114
|
+
_parseConversationTurns(agent) {
|
|
1065
1115
|
const logPath = agentLogPath(this.daemon.grooveDir, agent);
|
|
1066
1116
|
if (!existsSync(logPath)) return null;
|
|
1067
1117
|
|
|
@@ -1121,37 +1171,23 @@ export class Journalist {
|
|
|
1121
1171
|
merged.push({ ...turn });
|
|
1122
1172
|
}
|
|
1123
1173
|
}
|
|
1174
|
+
return merged;
|
|
1175
|
+
}
|
|
1124
1176
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
// Truncate this entry to fit remaining budget
|
|
1140
|
-
const remaining = maxChars - totalLen;
|
|
1141
|
-
if (remaining > 200) {
|
|
1142
|
-
kept.push(entry.slice(0, remaining) + '\n[...truncated]');
|
|
1143
|
-
}
|
|
1144
|
-
break;
|
|
1145
|
-
}
|
|
1146
|
-
kept.push(entry);
|
|
1147
|
-
totalLen += entry.length;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// Reverse back to chronological order
|
|
1151
|
-
kept.reverse();
|
|
1152
|
-
thread = kept.join('\n\n---\n\n');
|
|
1153
|
-
|
|
1154
|
-
return thread;
|
|
1177
|
+
/**
|
|
1178
|
+
* The agent's ORIGINAL task, from the first substantial user message in the
|
|
1179
|
+
* FULL log — never from the truncated resume window. When weeks of dialogue
|
|
1180
|
+
* exceed the resume budget, the window holds only recent work; anchoring the
|
|
1181
|
+
* task there makes the agent's identity drift onto whatever happened last.
|
|
1182
|
+
* The name-keyed log spans all rotations, so the true origin is always here.
|
|
1183
|
+
*/
|
|
1184
|
+
extractOriginalTask(agent) {
|
|
1185
|
+
const merged = this._parseConversationTurns(agent);
|
|
1186
|
+
if (!merged) return '';
|
|
1187
|
+
const first = merged.find((t) => t.role === 'user' && t.text.trim().length >= 10);
|
|
1188
|
+
if (!first) return '';
|
|
1189
|
+
const text = first.text.trim();
|
|
1190
|
+
return text.length > 500 ? text.slice(0, 500) + '...' : text;
|
|
1155
1191
|
}
|
|
1156
1192
|
|
|
1157
1193
|
/**
|
|
@@ -1159,15 +1195,17 @@ export class Journalist {
|
|
|
1159
1195
|
* thread so a fresh agent picks up where the previous session left off.
|
|
1160
1196
|
*/
|
|
1161
1197
|
buildConversationResumePrompt(agent, userMessage, { isRotation = false, reason } = {}) {
|
|
1162
|
-
const
|
|
1198
|
+
const budget = this.daemon.config?.resumeBudgetChars ?? DEFAULT_RESUME_BUDGET_CHARS;
|
|
1199
|
+
const thread = this.extractConversationThread(agent, { maxChars: budget });
|
|
1163
1200
|
if (!thread) return null;
|
|
1164
1201
|
|
|
1165
1202
|
const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
|
|
1166
1203
|
const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 5, 1000, agent.scope, agent.teamId) || '';
|
|
1167
1204
|
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
|
|
1205
|
+
// Anchor the agent to its ORIGINAL task from the full log — the truncated
|
|
1206
|
+
// thread's first message may be mid-project (or another agent's task) once
|
|
1207
|
+
// history outgrows the budget. Fall back to the window only if the log parse fails.
|
|
1208
|
+
const originalTask = this.extractOriginalTask(agent) || this._extractOriginalTask(thread);
|
|
1171
1209
|
|
|
1172
1210
|
// Rotation and idle-resume need very different framing. During rotation the agent
|
|
1173
1211
|
// has no new user message — it must continue the exact in-progress task without
|
|
@@ -1181,6 +1219,7 @@ export class Journalist {
|
|
|
1181
1219
|
originalTask ? `Your task: ${originalTask}` : '',
|
|
1182
1220
|
``,
|
|
1183
1221
|
`Rules:`,
|
|
1222
|
+
`- NEVER mention the rotation, context refresh, restart, or any session discontinuity to the user — from the user's perspective you are one continuous agent and this refresh must be invisible`,
|
|
1184
1223
|
`- Continue ONLY the task described in the conversation below`,
|
|
1185
1224
|
`- Do NOT explore the codebase looking for other things to fix or improve`,
|
|
1186
1225
|
`- Do NOT start new work that was not part of the original task`,
|
|
@@ -1210,14 +1249,16 @@ export class Journalist {
|
|
|
1210
1249
|
isIdleResume ? `## New Message From User\n\n${userMessage}` : '',
|
|
1211
1250
|
``,
|
|
1212
1251
|
isRotation
|
|
1213
|
-
? `Continue the in-progress task from the conversation above. Stay focused on that task only. Do not ask the user to repeat anything. If the task was already completed, state that and wait for new instructions.`
|
|
1214
|
-
: `Continue seamlessly from the conversation above. You have the full context of what was discussed, what was tried, what worked and what didn't. Do not ask the user to repeat anything.`,
|
|
1252
|
+
? `Continue the in-progress task from the conversation above. Stay focused on that task only. Do not ask the user to repeat anything, and never reference this context refresh — continue as if the conversation above simply never stopped. If the task was already completed, state that and wait for new instructions.`
|
|
1253
|
+
: `Continue seamlessly from the conversation above. You have the full context of what was discussed, what was tried, what worked and what didn't. Do not ask the user to repeat anything, and never mention the interruption or that you are resuming — to the user this is one continuous conversation.`,
|
|
1215
1254
|
].filter(Boolean).join('\n');
|
|
1216
1255
|
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1256
|
+
// Cap scales with the configured budget (framing adds constraints/discoveries
|
|
1257
|
+
// on top of the thread) to leave room in the context window
|
|
1258
|
+
const promptCap = budget + 20_000;
|
|
1259
|
+
if (prompt.length > promptCap) {
|
|
1219
1260
|
// Re-extract with smaller budget and rebuild
|
|
1220
|
-
const smallerThread = this.extractConversationThread(agent, { maxChars:
|
|
1261
|
+
const smallerThread = this.extractConversationThread(agent, { maxChars: Math.floor(budget * 2 / 3) });
|
|
1221
1262
|
if (smallerThread) {
|
|
1222
1263
|
prompt = [
|
|
1223
1264
|
`# Session Context Resume`,
|
|
@@ -1238,8 +1279,8 @@ export class Journalist {
|
|
|
1238
1279
|
isIdleResume ? `## New Message From User\n\n${userMessage}` : '',
|
|
1239
1280
|
``,
|
|
1240
1281
|
isRotation
|
|
1241
|
-
? `Continue the in-progress task only. Do not explore or start new work. If done, state that and wait.`
|
|
1242
|
-
: `Continue seamlessly. Do not ask the user to repeat anything.`,
|
|
1282
|
+
? `Continue the in-progress task only. Do not explore or start new work. Never mention this context refresh to the user. If done, state that and wait.`
|
|
1283
|
+
: `Continue seamlessly. Do not ask the user to repeat anything, and never mention the interruption — to the user this is one continuous conversation.`,
|
|
1243
1284
|
].filter(Boolean).join('\n');
|
|
1244
1285
|
}
|
|
1245
1286
|
}
|
|
@@ -18,7 +18,12 @@ const SCORE_HISTORY_MAX = 40; // ~10 min at 15s intervals
|
|
|
18
18
|
const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes between rotations per agent
|
|
19
19
|
const QUALITY_COOLDOWN_MS = 2 * 60 * 1000; // 2 minutes for quality degradation rotations
|
|
20
20
|
const TOKEN_CEILING = 5_000_000; // 5M tokens per agent (non-self-managing only)
|
|
21
|
-
|
|
21
|
+
// Self-managing providers: rotate idle agents above this context usage.
|
|
22
|
+
// 0.80 favors context retention over cache-read cost — the conversation-resume
|
|
23
|
+
// window is a slice of the full session, so every rotation is lossy; for
|
|
24
|
+
// complex long-lived work that loss compounds faster than token savings pay off.
|
|
25
|
+
// Lower via config (replayCeiling) for cost-sensitive fleets; 0 disables.
|
|
26
|
+
const REPLAY_CEILING = 0.80;
|
|
22
27
|
const VELOCITY_CEILING = 250_000; // New (non-cache) tokens per window before force rotation
|
|
23
28
|
const VELOCITY_WINDOW_MS = 5 * 60_000;
|
|
24
29
|
const ROLE_MULTIPLIERS = {
|