clementine-agent 1.18.20 → 1.18.22
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/README.md +17 -0
- package/dist/agent/action-enforcer.d.ts +29 -0
- package/dist/agent/action-enforcer.js +120 -0
- package/dist/agent/assistant.d.ts +12 -0
- package/dist/agent/assistant.js +165 -31
- package/dist/agent/auto-update.js +46 -2
- package/dist/agent/local-turn.d.ts +16 -0
- package/dist/agent/local-turn.js +54 -1
- package/dist/agent/route-classifier.d.ts +1 -0
- package/dist/agent/route-classifier.js +30 -3
- package/dist/agent/toolsets.d.ts +14 -0
- package/dist/agent/toolsets.js +68 -0
- package/dist/brain/ingestion-pipeline.d.ts +7 -0
- package/dist/brain/ingestion-pipeline.js +107 -21
- package/dist/channels/discord.js +38 -7
- package/dist/channels/telegram.js +5 -6
- package/dist/cli/dashboard.js +1071 -70
- package/dist/cli/index.js +174 -0
- package/dist/cli/ingest.js +8 -2
- package/dist/gateway/context-hygiene.d.ts +17 -0
- package/dist/gateway/context-hygiene.js +31 -0
- package/dist/gateway/heartbeat-scheduler.d.ts +20 -0
- package/dist/gateway/heartbeat-scheduler.js +27 -10
- package/dist/gateway/router.d.ts +7 -0
- package/dist/gateway/router.js +303 -9
- package/dist/gateway/turn-ledger.d.ts +32 -0
- package/dist/gateway/turn-ledger.js +55 -0
- package/dist/memory/embeddings.d.ts +2 -0
- package/dist/memory/embeddings.js +8 -1
- package/dist/memory/store.d.ts +88 -1
- package/dist/memory/store.js +349 -18
- package/dist/memory/write-queue.d.ts +16 -0
- package/dist/memory/write-queue.js +5 -0
- package/dist/tools/shared.d.ts +89 -0
- package/dist/types.d.ts +11 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +56 -6
package/README.md
CHANGED
|
@@ -216,10 +216,27 @@ clementine login | auth Authenticate Claude Code / OAuth providers
|
|
|
216
216
|
clementine chat Interactive REPL
|
|
217
217
|
clementine memory status Index size, recent activity
|
|
218
218
|
clementine memory search <q> FTS5 search
|
|
219
|
+
clementine memory model status Local dense embedding model cache
|
|
220
|
+
clementine memory model install Pre-cache the local embedding model
|
|
219
221
|
clementine memory dedup | reembed Maintenance
|
|
220
222
|
clementine brain digest Run the brain digest pipeline
|
|
221
223
|
```
|
|
222
224
|
|
|
225
|
+
Dense neural recall uses a local Transformers.js embedding model. Model
|
|
226
|
+
weights are not bundled into the npm tarball; the first install caches them
|
|
227
|
+
under `~/.clementine/models/`. To make repo or npm updates prefetch the model
|
|
228
|
+
automatically, set this once in `~/.clementine/.env`:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
CLEMENTINE_PREFETCH_EMBEDDINGS=1
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
You can also opt in for a single install/update command:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
CLEMENTINE_INSTALL_EMBEDDINGS=1 npm install -g clementine-agent
|
|
238
|
+
```
|
|
239
|
+
|
|
223
240
|
**Projects & agents**
|
|
224
241
|
```
|
|
225
242
|
clementine projects list | add <p> | remove <p>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type ActionExpectationSource = 'approval_followup' | 'user_request' | 'diagnostic_request' | 'none';
|
|
2
|
+
export interface ActionExpectation {
|
|
3
|
+
expected: boolean;
|
|
4
|
+
source: ActionExpectationSource;
|
|
5
|
+
reason: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ActionResponseAssessment {
|
|
8
|
+
violation: boolean;
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function detectActionExpectation(userText: string, opts?: {
|
|
12
|
+
approvalFollowup?: boolean;
|
|
13
|
+
}): ActionExpectation;
|
|
14
|
+
export declare function buildApprovalFollowupPrompt(reply: string): string;
|
|
15
|
+
export declare function assessActionResponse(input: {
|
|
16
|
+
actionExpectation: ActionExpectation;
|
|
17
|
+
userText: string;
|
|
18
|
+
response: string;
|
|
19
|
+
toolActivityCount: number;
|
|
20
|
+
backgroundStarted?: boolean;
|
|
21
|
+
delegated?: boolean;
|
|
22
|
+
}): ActionResponseAssessment;
|
|
23
|
+
export declare function buildActionEnforcementPrompt(input: {
|
|
24
|
+
userText: string;
|
|
25
|
+
previousResponse: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}): string;
|
|
28
|
+
export declare function fallbackUnverifiedActionResponse(reason: string): string;
|
|
29
|
+
//# sourceMappingURL=action-enforcer.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { looksLikeApprovalPrompt } from './local-turn.js';
|
|
2
|
+
function normalize(text) {
|
|
3
|
+
return text.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
4
|
+
}
|
|
5
|
+
const ACTION_REQUEST_RE = /\b(can you|could you|would you|please|pls|i need you to|i want you to|let'?s|go ahead and|do it|handle this|take care of)\b[\s\S]{0,160}\b(send|email|message|post|publish|delete|change|update|run|execute|check|look(?:\s+into)?|diagnose|investigate|figure(?:\s+it)?\s*out|find|search|read|write|create|fix|schedule|reschedule|pull|fetch|review|tag|save|upload|download)\b/i;
|
|
6
|
+
const DIRECT_ACTION_RE = /^(send|email|message|post|publish|delete|change|update|run|execute|check|look(?:\s+into)?|diagnose|investigate|find|search|read|write|create|fix|schedule|reschedule|pull|fetch|review|tag|save|upload|download)\b/i;
|
|
7
|
+
const DIAGNOSTIC_RE = /\b(log|logs|crash|crashing|error|failing|failure|broken|diagnose|debug|investigate|look into|figure it out|what'?s causing|why is|why did)\b/i;
|
|
8
|
+
const DONE_CLAIM_RE = /\b(done|sent|emailed|queued|accepted|completed|finished|fixed|updated|changed|deleted|posted|published|scheduled|rescheduled|created|saved|uploaded|downloaded|tagged|checked|reviewed|found|read|ran|executed)\b/i;
|
|
9
|
+
const PROMISE_RE = /\b(i'?ll|i will|i am going to|i'?m going to|let me|i'?m checking|i'?m sending|i'?m running|i'?m looking|working on it|on it)\b[\s\S]{0,120}\b(send|email|message|post|publish|delete|change|update|run|execute|check|look|diagnose|investigate|find|search|read|write|create|fix|schedule|reschedule|pull|fetch|review|tag|save|upload|download|now)\b/i;
|
|
10
|
+
const VACUOUS_ACK_RE = /^(got it|okay|ok|sure|perfect|sounds good|on it|will do|yep|yeah)[.! ]*$/i;
|
|
11
|
+
const BLOCKED_OR_ASKING_RE = /\b(i can'?t|i cannot|unable to|blocked|need you to|need a|need the|please provide|please send|can you send|can you share|which|what should|who should|before i|confirm|approve|good to go|okay to)\b/i;
|
|
12
|
+
const DIAGNOSTIC_DEFLECTION_RE = /\b(what are you seeing|what do you see|send (me )?the logs|share (the )?logs|provide (the )?logs|can you paste|can you send me)\b/i;
|
|
13
|
+
export function detectActionExpectation(userText, opts = {}) {
|
|
14
|
+
if (opts.approvalFollowup) {
|
|
15
|
+
return {
|
|
16
|
+
expected: true,
|
|
17
|
+
source: 'approval_followup',
|
|
18
|
+
reason: 'user approved the previous action prompt',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const text = userText.trim();
|
|
22
|
+
if (!text)
|
|
23
|
+
return { expected: false, source: 'none', reason: 'empty message' };
|
|
24
|
+
if (ACTION_REQUEST_RE.test(text) || DIRECT_ACTION_RE.test(text)) {
|
|
25
|
+
const diagnostic = DIAGNOSTIC_RE.test(text);
|
|
26
|
+
return {
|
|
27
|
+
expected: true,
|
|
28
|
+
source: diagnostic ? 'diagnostic_request' : 'user_request',
|
|
29
|
+
reason: diagnostic ? 'user asked for local/tool-backed diagnosis' : 'user asked Clementine to take an action',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (DIAGNOSTIC_RE.test(text) && /\b(can you|could you|please|figure|diagnose|debug|look)\b/i.test(text)) {
|
|
33
|
+
return {
|
|
34
|
+
expected: true,
|
|
35
|
+
source: 'diagnostic_request',
|
|
36
|
+
reason: 'user asked for local/tool-backed diagnosis',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { expected: false, source: 'none', reason: 'no concrete action requested' };
|
|
40
|
+
}
|
|
41
|
+
export function buildApprovalFollowupPrompt(reply) {
|
|
42
|
+
return [
|
|
43
|
+
`[Approval reply: "${reply.trim().slice(0, 120)}"]`,
|
|
44
|
+
'The user approved the action you proposed in your previous message.',
|
|
45
|
+
'Continue from that previous approval prompt and perform the approved action now using the appropriate tool.',
|
|
46
|
+
'Do not treat this as casual feedback. Do not say it is done unless a tool call verifies it. If you are blocked, say exactly what is blocking you.',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
export function assessActionResponse(input) {
|
|
50
|
+
const { actionExpectation, userText, response, toolActivityCount } = input;
|
|
51
|
+
if (!actionExpectation.expected)
|
|
52
|
+
return { violation: false, reason: 'no action expected' };
|
|
53
|
+
if (input.backgroundStarted)
|
|
54
|
+
return { violation: false, reason: 'action was queued in background' };
|
|
55
|
+
if (input.delegated)
|
|
56
|
+
return { violation: false, reason: 'action was delegated' };
|
|
57
|
+
if (toolActivityCount > 0)
|
|
58
|
+
return { violation: false, reason: 'tool activity observed' };
|
|
59
|
+
const trimmed = response.trim();
|
|
60
|
+
if (!trimmed)
|
|
61
|
+
return { violation: true, reason: 'empty response to action request' };
|
|
62
|
+
if (looksLikeApprovalPrompt(trimmed)) {
|
|
63
|
+
if (actionExpectation.source === 'approval_followup') {
|
|
64
|
+
return { violation: true, reason: 'asked for approval again after the user already approved' };
|
|
65
|
+
}
|
|
66
|
+
return { violation: false, reason: 'assistant requested approval before acting' };
|
|
67
|
+
}
|
|
68
|
+
const lower = normalize(trimmed);
|
|
69
|
+
if (actionExpectation.source === 'diagnostic_request' && DIAGNOSTIC_DEFLECTION_RE.test(lower)) {
|
|
70
|
+
return { violation: true, reason: 'asked user for logs instead of using available local tools' };
|
|
71
|
+
}
|
|
72
|
+
if (VACUOUS_ACK_RE.test(trimmed)) {
|
|
73
|
+
return { violation: true, reason: 'acknowledged action request without acting' };
|
|
74
|
+
}
|
|
75
|
+
if (DONE_CLAIM_RE.test(trimmed)) {
|
|
76
|
+
return { violation: true, reason: 'claimed completion without tool activity' };
|
|
77
|
+
}
|
|
78
|
+
if (PROMISE_RE.test(trimmed)) {
|
|
79
|
+
return { violation: true, reason: 'promised future action without same-turn tool activity' };
|
|
80
|
+
}
|
|
81
|
+
if (BLOCKED_OR_ASKING_RE.test(lower) || trimmed.endsWith('?')) {
|
|
82
|
+
return { violation: false, reason: 'assistant asked for needed input or reported a block' };
|
|
83
|
+
}
|
|
84
|
+
// Diagnostic requests should usually use local tools. A generic answer with
|
|
85
|
+
// no tool activity is allowed only if it clearly does not claim inspection.
|
|
86
|
+
if (actionExpectation.source === 'diagnostic_request' && /\b(i think|likely|probably|sounds like)\b/i.test(trimmed)) {
|
|
87
|
+
return { violation: false, reason: 'assistant gave hypothesis without claiming inspection' };
|
|
88
|
+
}
|
|
89
|
+
// For action-shaped requests, a generic short answer is usually a stall.
|
|
90
|
+
if (trimmed.length < 80 && /\b(send|email|message|run|fix|check|diagnose|figure|look)\b/i.test(userText)) {
|
|
91
|
+
return { violation: true, reason: 'short action response without tool activity' };
|
|
92
|
+
}
|
|
93
|
+
return { violation: false, reason: 'no unsupported action claim detected' };
|
|
94
|
+
}
|
|
95
|
+
export function buildActionEnforcementPrompt(input) {
|
|
96
|
+
return [
|
|
97
|
+
'[SYSTEM ACTION ENFORCEMENT]',
|
|
98
|
+
'Your previous response was not allowed because it implied action without verified tool activity.',
|
|
99
|
+
`Reason: ${input.reason}`,
|
|
100
|
+
'',
|
|
101
|
+
'Original user request:',
|
|
102
|
+
input.userText.slice(0, 1200),
|
|
103
|
+
'',
|
|
104
|
+
'Previous response:',
|
|
105
|
+
input.previousResponse.slice(0, 1200),
|
|
106
|
+
'',
|
|
107
|
+
'Now correct this in the same turn:',
|
|
108
|
+
'- If the action is possible, use the appropriate tool now.',
|
|
109
|
+
'- If the action is blocked, say exactly what is blocking it.',
|
|
110
|
+
'- Do not say "done", "sent", "queued", "checked", or similar unless a tool call actually verifies it.',
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
export function fallbackUnverifiedActionResponse(reason) {
|
|
114
|
+
return [
|
|
115
|
+
"I didn't complete that yet.",
|
|
116
|
+
`I caught an action-verification issue: ${reason}.`,
|
|
117
|
+
"I won't call it done without a tool confirmation. Please resend the request and I'll retry from a clean turn.",
|
|
118
|
+
].join(' ');
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=action-enforcer.js.map
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { AgentProfile, OnTextCallback, OnToolActivityCallback, VerboseLevel } from '../types.js';
|
|
13
13
|
import { AgentManager } from './agent-manager.js';
|
|
14
|
+
import { type ToolsetName } from './toolsets.js';
|
|
14
15
|
/**
|
|
15
16
|
* Estimate token count for Claude.
|
|
16
17
|
*
|
|
@@ -30,6 +31,8 @@ export declare function estimateTokens(text: string): number;
|
|
|
30
31
|
export declare function looksLikeContextThrashText(value: unknown): boolean;
|
|
31
32
|
export declare function contextThrashRecoveryNotice(): string;
|
|
32
33
|
export declare function buildContextThrashRecoveryPrompt(userRequest: string, priorFailureText?: string): string;
|
|
34
|
+
/** Format a millisecond duration as a human-friendly "X ago" string. */
|
|
35
|
+
export declare function formatTimeAgo(ms: number): string;
|
|
33
36
|
export declare function looksLikeOneMillionContextError(value: unknown): boolean;
|
|
34
37
|
export declare function oneMillionContextRecoveryMessage(): string;
|
|
35
38
|
export declare function looksLikeProviderApiErrorResponse(value: unknown): boolean;
|
|
@@ -147,6 +150,7 @@ export declare class PersonalAssistant {
|
|
|
147
150
|
flushSessions(): void;
|
|
148
151
|
private saveSessionsNow;
|
|
149
152
|
getExchangeCount(sessionKey: string): number;
|
|
153
|
+
hasRecentApprovalPrompt(sessionKey: string): boolean;
|
|
150
154
|
getMemoryChunkCount(): number;
|
|
151
155
|
private buildSystemPrompt;
|
|
152
156
|
private buildOptions;
|
|
@@ -178,6 +182,7 @@ export declare class PersonalAssistant {
|
|
|
178
182
|
projectOverride?: ProjectMeta;
|
|
179
183
|
verboseLevel?: VerboseLevel;
|
|
180
184
|
abortController?: AbortController;
|
|
185
|
+
toolset?: ToolsetName;
|
|
181
186
|
}): Promise<[string, string]>;
|
|
182
187
|
/**
|
|
183
188
|
* Compare retrieved chunks against the response text and record which
|
|
@@ -198,6 +203,12 @@ export declare class PersonalAssistant {
|
|
|
198
203
|
* No LLM call — uses buildLocalSummary for instant summarization.
|
|
199
204
|
*/
|
|
200
205
|
private compactContext;
|
|
206
|
+
compactSessionForGateway(sessionKey: string, reason?: string): {
|
|
207
|
+
compacted: boolean;
|
|
208
|
+
exchangeCount: number;
|
|
209
|
+
summary?: string;
|
|
210
|
+
reason: string;
|
|
211
|
+
};
|
|
201
212
|
/**
|
|
202
213
|
* Expire sessions inactive for more than 24 hours.
|
|
203
214
|
* Called periodically from chat() to prevent unbounded map growth.
|
|
@@ -209,6 +220,7 @@ export declare class PersonalAssistant {
|
|
|
209
220
|
* to avoid blocking the user's query.
|
|
210
221
|
*/
|
|
211
222
|
private buildLocalSummary;
|
|
223
|
+
private buildStructuredCompactionSummary;
|
|
212
224
|
private buildLocalSummaryFromTurns;
|
|
213
225
|
/**
|
|
214
226
|
* Walk a chronological list of transcript turns and pair adjacent
|
package/dist/agent/assistant.js
CHANGED
|
@@ -21,7 +21,7 @@ import { detectFrustrationSignals, detectRepeatedTopics } from './insight-engine
|
|
|
21
21
|
import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
|
|
22
22
|
import { enforceToolPermissions, getSecurityPrompt, getHeartbeatSecurityPrompt, getCronSecurityPrompt, getHeartbeatDisallowedTools, logToolUse, setProfileTier, setProfileAllowedTools, setAgentDir, setSendPolicy, setInteractionSource, logAuditJsonl, } from './hooks.js';
|
|
23
23
|
import { scanner } from '../security/scanner.js';
|
|
24
|
-
import { agentWorkingMemoryFile, listAllGoals } from '../tools/shared.js';
|
|
24
|
+
import { agentWorkingMemoryFile, capOutput, listAllGoals } from '../tools/shared.js';
|
|
25
25
|
import { AgentManager } from './agent-manager.js';
|
|
26
26
|
import { extractLinks } from './link-extractor.js';
|
|
27
27
|
import { StallGuard } from './stall-guard.js';
|
|
@@ -33,6 +33,8 @@ import { searchSkills as searchSkillsSync } from './skill-extractor.js';
|
|
|
33
33
|
import { classifyIntent, getStrategyGuidance } from './intent-classifier.js';
|
|
34
34
|
import { getEventLog } from './session-event-log.js';
|
|
35
35
|
import { routeToolSurface, TOOL_SURFACE_HARD_LIMIT, TOOL_SURFACE_WARN_THRESHOLD } from './tool-router.js';
|
|
36
|
+
import { isRestrictedToolset, toolsetAllowsLocalWrites } from './toolsets.js';
|
|
37
|
+
import { looksLikeApprovalPrompt } from './local-turn.js';
|
|
36
38
|
import { decideTurn } from './turn-policy.js';
|
|
37
39
|
import { loadClementineJson } from '../config/clementine-json.js';
|
|
38
40
|
import { isCreditBalanceError, markBackgroundCreditBlocked } from '../gateway/credit-guard.js';
|
|
@@ -294,9 +296,23 @@ const query = ((args) => {
|
|
|
294
296
|
}
|
|
295
297
|
return rawQuery(args);
|
|
296
298
|
});
|
|
299
|
+
function parseMemoryTimestampMs(value) {
|
|
300
|
+
const text = String(value ?? '').trim();
|
|
301
|
+
if (!text)
|
|
302
|
+
return NaN;
|
|
303
|
+
// SQLite datetime('now') returns UTC as "YYYY-MM-DD HH:mm:ss" with no zone.
|
|
304
|
+
// Parse it explicitly as UTC so summaries don't appear hours in the future.
|
|
305
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(text)) {
|
|
306
|
+
return Date.parse(`${text.replace(' ', 'T')}Z`);
|
|
307
|
+
}
|
|
308
|
+
return Date.parse(text);
|
|
309
|
+
}
|
|
297
310
|
/** Format a millisecond duration as a human-friendly "X ago" string. */
|
|
298
|
-
function formatTimeAgo(ms) {
|
|
299
|
-
const
|
|
311
|
+
export function formatTimeAgo(ms) {
|
|
312
|
+
const safeMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
|
|
313
|
+
if (safeMs < 60_000)
|
|
314
|
+
return 'just now';
|
|
315
|
+
const minutes = Math.floor(safeMs / 60_000);
|
|
300
316
|
if (minutes < 60)
|
|
301
317
|
return `${minutes}m ago`;
|
|
302
318
|
const hours = Math.floor(minutes / 60);
|
|
@@ -311,6 +327,11 @@ function formatTimeAgo(ms) {
|
|
|
311
327
|
const CONTEXT_GUARD_MIN_TOKENS = 16_000;
|
|
312
328
|
/** Warn threshold — context is getting tight. */
|
|
313
329
|
const CONTEXT_GUARD_WARN_TOKENS = 32_000;
|
|
330
|
+
const PENDING_CONTEXT_USER_MAX_CHARS = 1000;
|
|
331
|
+
const PENDING_CONTEXT_ASSISTANT_MAX_CHARS = 3000;
|
|
332
|
+
const CRON_PROGRESS_NOTES_MAX_CHARS = 2000;
|
|
333
|
+
const CRON_PROGRESS_PENDING_MAX_ITEMS = 20;
|
|
334
|
+
const CRON_PROGRESS_ITEM_MAX_CHARS = 300;
|
|
314
335
|
/** Rotate SDK sessions before hidden resume history approaches the 200K cap. */
|
|
315
336
|
const SESSION_ROTATE_INPUT_TOKENS = 140_000;
|
|
316
337
|
/** Approximate context window sizes by model family. */
|
|
@@ -328,6 +349,12 @@ function getContextWindow(model) {
|
|
|
328
349
|
}
|
|
329
350
|
return 200_000; // safe default
|
|
330
351
|
}
|
|
352
|
+
function capContextBlock(text, maxChars) {
|
|
353
|
+
return capOutput(String(text ?? ''), maxChars);
|
|
354
|
+
}
|
|
355
|
+
function capContextItem(text) {
|
|
356
|
+
return capContextBlock(text, CRON_PROGRESS_ITEM_MAX_CHARS).replace(/\s+/g, ' ').trim();
|
|
357
|
+
}
|
|
331
358
|
function resultInputTokens(result) {
|
|
332
359
|
let total = 0;
|
|
333
360
|
const modelUsage = result.modelUsage;
|
|
@@ -1319,6 +1346,10 @@ export class PersonalAssistant {
|
|
|
1319
1346
|
getExchangeCount(sessionKey) {
|
|
1320
1347
|
return this.exchangeCounts.get(sessionKey) ?? 0;
|
|
1321
1348
|
}
|
|
1349
|
+
hasRecentApprovalPrompt(sessionKey) {
|
|
1350
|
+
const lastAssistant = this.lastExchanges.get(sessionKey)?.at(-1)?.assistant ?? '';
|
|
1351
|
+
return looksLikeApprovalPrompt(lastAssistant);
|
|
1352
|
+
}
|
|
1322
1353
|
getMemoryChunkCount() {
|
|
1323
1354
|
if (!this.memoryStore)
|
|
1324
1355
|
return 0;
|
|
@@ -1950,7 +1981,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1950
1981
|
}
|
|
1951
1982
|
// ── Build SDK Options ─────────────────────────────────────────────
|
|
1952
1983
|
async buildOptions(opts = {}) {
|
|
1953
|
-
const { isHeartbeat = false, cronTier = null, maxTurns = null, model = null, enableTeams = true, retrievalContext = '', profile = null, sessionKey = null, streaming = false, isPlanStep = false, isUnleashed = false, sourceOverride, disableAllTools = false, verboseLevel, abortController, effort, maxBudgetUsd, toolScopeText, thinking, outputFormat, stallGuard, intentClassification, turnPolicy, contextRoutingText, } = opts;
|
|
1984
|
+
const { isHeartbeat = false, cronTier = null, maxTurns = null, model = null, enableTeams = true, retrievalContext = '', profile = null, sessionKey = null, streaming = false, isPlanStep = false, isUnleashed = false, sourceOverride, disableAllTools = false, verboseLevel, abortController, effort, maxBudgetUsd, toolScopeText, thinking, outputFormat, stallGuard, intentClassification, turnPolicy, contextRoutingText, toolset = 'auto', } = opts;
|
|
1954
1985
|
const isCron = cronTier !== null;
|
|
1955
1986
|
const toolsDisabledForCall = disableAllTools || (isHeartbeat && !isCron);
|
|
1956
1987
|
const promptScopeText = toolScopeText ?? '';
|
|
@@ -2001,7 +2032,27 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2001
2032
|
const safeContextToolRoute = allowContextToolRoute && !contextToolRoute.fullSurface
|
|
2002
2033
|
? contextToolRoute
|
|
2003
2034
|
: emptyToolRoute();
|
|
2004
|
-
|
|
2035
|
+
let toolRoute = mergeToolRoutes(promptToolRoute, mergeToolRoutes(safeProfileToolRoute, safeContextToolRoute));
|
|
2036
|
+
if (toolset === 'full') {
|
|
2037
|
+
toolRoute = {
|
|
2038
|
+
bundles: [],
|
|
2039
|
+
externalMcpServers: undefined,
|
|
2040
|
+
composioToolkits: undefined,
|
|
2041
|
+
inheritFullClaudeEnv: true,
|
|
2042
|
+
fullSurface: true,
|
|
2043
|
+
reason: 'full_surface',
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
else if (isRestrictedToolset(toolset)) {
|
|
2047
|
+
toolRoute = {
|
|
2048
|
+
...toolRoute,
|
|
2049
|
+
bundles: [],
|
|
2050
|
+
externalMcpServers: [],
|
|
2051
|
+
composioToolkits: [],
|
|
2052
|
+
inheritFullClaudeEnv: false,
|
|
2053
|
+
fullSurface: false,
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2005
2056
|
let allowedTools = [];
|
|
2006
2057
|
const addAllowed = (...tools) => {
|
|
2007
2058
|
for (const tool of tools) {
|
|
@@ -2021,9 +2072,13 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2021
2072
|
const memoryNeeded = autonomousToolRun
|
|
2022
2073
|
|| retrievalContext.trim().length > 0
|
|
2023
2074
|
|| (turnPolicy?.retrievalTier !== undefined && turnPolicy.retrievalTier !== 'none');
|
|
2024
|
-
const localReadNeeded = taskIntent || /\b(repo|repository|code|file|files|folder|directory|path|log|logs|config|read|show|grep|diff|search)\b/i.test(promptScopeLower);
|
|
2025
|
-
const
|
|
2026
|
-
|
|
2075
|
+
const localReadNeeded = taskIntent || toolset === 'diagnostic' || /\b(repo|repository|code|file|files|folder|directory|path|log|logs|config|read|show|grep|diff|search)\b/i.test(promptScopeLower);
|
|
2076
|
+
const diagnosticCommandNeeded = toolset === 'diagnostic'
|
|
2077
|
+
&& /\b(run|test|npm|pnpm|yarn|node|git|logs?|tail|ps|status|diagnos(?:e|tic)|check)\b/i.test(promptScopeLower);
|
|
2078
|
+
const localWriteNeeded = diagnosticCommandNeeded
|
|
2079
|
+
|| (toolsetAllowsLocalWrites(toolset) && (taskIntent || /\b(write|edit|fix|implement|refactor|build|test|run|npm|git|commit|push|pull|deploy|install|configure)\b/i.test(promptScopeLower)));
|
|
2080
|
+
const adminNeeded = toolRoute.fullSurface
|
|
2081
|
+
|| (toolsetAllowsLocalWrites(toolset) && /\b(self[- ]?update|restart|daemon|doctor|env|credential|integration|setup|set up|configure|npm publish|publish to npm)\b/i.test(promptScopeLower));
|
|
2027
2082
|
if (!toolsDisabledForCall) {
|
|
2028
2083
|
if (toolRoute.fullSurface) {
|
|
2029
2084
|
addAllowed('Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'WebSearch', 'WebFetch');
|
|
@@ -2032,8 +2087,12 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2032
2087
|
else {
|
|
2033
2088
|
if (localReadNeeded)
|
|
2034
2089
|
addAllowed('Read', 'Glob', 'Grep');
|
|
2035
|
-
if (localWriteNeeded)
|
|
2036
|
-
|
|
2090
|
+
if (localWriteNeeded) {
|
|
2091
|
+
if (toolset === 'diagnostic')
|
|
2092
|
+
addAllowed('Bash');
|
|
2093
|
+
else
|
|
2094
|
+
addAllowed('Write', 'Edit', 'Bash');
|
|
2095
|
+
}
|
|
2037
2096
|
if (toolRoute.bundles.includes('web_research') || toolRoute.bundles.includes('docs_lookup')) {
|
|
2038
2097
|
addAllowed('WebSearch', 'WebFetch');
|
|
2039
2098
|
}
|
|
@@ -2041,7 +2100,12 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2041
2100
|
addClementineTools(CLEMENTINE_CORE_TOOL_NAMES);
|
|
2042
2101
|
addClementineTools(CLEMENTINE_RELATIONSHIP_TOOL_NAMES);
|
|
2043
2102
|
}
|
|
2044
|
-
|
|
2103
|
+
const clementineMemoryWritesAllowed = toolset === 'auto'
|
|
2104
|
+
|| toolset === 'full'
|
|
2105
|
+
|| toolset === 'communications'
|
|
2106
|
+
|| intentClassification?.type === 'feedback'
|
|
2107
|
+
|| intentClassification?.type === 'correction';
|
|
2108
|
+
if ((taskIntent || intentClassification?.type === 'correction') && clementineMemoryWritesAllowed) {
|
|
2045
2109
|
addClementineTools(CLEMENTINE_MEMORY_WRITE_TOOL_NAMES);
|
|
2046
2110
|
addClementineTools(CLEMENTINE_WORKSPACE_TOOL_NAMES);
|
|
2047
2111
|
}
|
|
@@ -2058,20 +2122,22 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2058
2122
|
addClementineTools(CLEMENTINE_INTEGRATION_TOOL_NAMES);
|
|
2059
2123
|
addClementineTools(CLEMENTINE_ADMIN_TOOL_NAMES);
|
|
2060
2124
|
}
|
|
2061
|
-
if (
|
|
2125
|
+
if ((toolset === 'auto' || toolset === 'full' || toolset === 'communications')
|
|
2126
|
+
&& (toolRoute.bundles.includes('email_outlook') || /\b(outlook|email|mailbox|inbox|calendar|follow-?up)\b/i.test(scopeText))) {
|
|
2062
2127
|
addClementineTools(CLEMENTINE_COMM_TOOL_NAMES);
|
|
2063
2128
|
}
|
|
2064
|
-
if (
|
|
2129
|
+
if ((toolset === 'auto' || toolset === 'full')
|
|
2130
|
+
&& (toolRoute.bundles.includes('github') || toolRoute.bundles.includes('browser') || toolRoute.bundles.includes('web_research'))) {
|
|
2065
2131
|
addClementineTools(CLEMENTINE_RESEARCH_TOOL_NAMES);
|
|
2066
2132
|
}
|
|
2067
|
-
if (enableTeams) {
|
|
2133
|
+
if (enableTeams && (toolset === 'auto' || toolset === 'full')) {
|
|
2068
2134
|
addAllowed('Task', 'Agent');
|
|
2069
2135
|
addClementineTools(CLEMENTINE_TEAM_TOOL_NAMES);
|
|
2070
2136
|
addClementineTools(CLEMENTINE_JOB_TOOL_NAMES);
|
|
2071
2137
|
}
|
|
2072
2138
|
}
|
|
2073
2139
|
// Include local user scripts/plugins for task-like or explicit full-surface turns.
|
|
2074
|
-
if (taskIntent || toolRoute.fullSurface || adminNeeded) {
|
|
2140
|
+
if (toolsetAllowsLocalWrites(toolset) && (taskIntent || toolRoute.fullSurface || adminNeeded)) {
|
|
2075
2141
|
try {
|
|
2076
2142
|
const toolsDir = path.join(BASE_DIR, 'tools');
|
|
2077
2143
|
const pluginsDir = path.join(BASE_DIR, 'plugins');
|
|
@@ -2414,6 +2480,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2414
2480
|
isolateClaudeConfig,
|
|
2415
2481
|
inheritFullClaudeEnv: shouldInheritClaudeEnv,
|
|
2416
2482
|
maxBudgetUsd: enforcedBudget,
|
|
2483
|
+
toolset,
|
|
2417
2484
|
isCron,
|
|
2418
2485
|
cronTier,
|
|
2419
2486
|
isPlanStep,
|
|
@@ -2806,6 +2873,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2806
2873
|
const projectOverride = options?.projectOverride;
|
|
2807
2874
|
const verboseLevel = options?.verboseLevel;
|
|
2808
2875
|
const abortController = options?.abortController;
|
|
2876
|
+
const toolset = options?.toolset ?? 'auto';
|
|
2809
2877
|
const key = sessionKey ?? undefined;
|
|
2810
2878
|
this._lastUserMessage = text;
|
|
2811
2879
|
let sessionRotated = false;
|
|
@@ -2906,11 +2974,14 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2906
2974
|
const exchanges = this.lastExchanges.get(key) ?? [];
|
|
2907
2975
|
if (exchanges.length === 0 && this.memoryStore) {
|
|
2908
2976
|
try {
|
|
2909
|
-
const recentSummaries = this.memoryStore.
|
|
2977
|
+
const recentSummaries = typeof this.memoryStore.getRecentSummariesForSession === 'function'
|
|
2978
|
+
? this.memoryStore.getRecentSummariesForSession(key, 1)
|
|
2979
|
+
: this.memoryStore.getRecentSummaries(5).filter((s) => s.sessionKey === key).slice(0, 1);
|
|
2910
2980
|
if (recentSummaries.length > 0) {
|
|
2911
2981
|
const last = recentSummaries[0];
|
|
2912
|
-
const
|
|
2913
|
-
|
|
2982
|
+
const createdAtMs = parseMemoryTimestampMs(last.createdAt);
|
|
2983
|
+
const ageMs = Date.now() - createdAtMs;
|
|
2984
|
+
if (Number.isFinite(ageMs) && ageMs >= -5 * 60_000 && ageMs < 7 * 24 * 60 * 60 * 1000) { // within 7 days
|
|
2914
2985
|
const ago = formatTimeAgo(ageMs);
|
|
2915
2986
|
effectivePrompt =
|
|
2916
2987
|
`[Last conversation (${ago}):\n${last.summary.slice(0, 600)}]\n\n` +
|
|
@@ -2946,7 +3017,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2946
3017
|
if (allPending.length > 0) {
|
|
2947
3018
|
const contextLines = [];
|
|
2948
3019
|
for (const ctx of allPending) {
|
|
2949
|
-
|
|
3020
|
+
const user = capContextBlock(ctx.user, PENDING_CONTEXT_USER_MAX_CHARS);
|
|
3021
|
+
const assistant = capContextBlock(ctx.assistant, PENDING_CONTEXT_ASSISTANT_MAX_CHARS);
|
|
3022
|
+
contextLines.push(`[${user}]\n${assistant}`);
|
|
2950
3023
|
}
|
|
2951
3024
|
effectivePrompt =
|
|
2952
3025
|
`[Since we last talked, you did some background work. Naturally mention what happened — lead with anything that needs attention, briefly note routine completions. Don't dump raw tool calls or list job names. Be conversational.\nBackground:\n${contextLines.join('\n\n')}]\n\n${effectivePrompt}`;
|
|
@@ -2975,7 +3048,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2975
3048
|
const effectiveMaxTurns = maxTurns ?? turnPolicy.maxTurns;
|
|
2976
3049
|
const CHAT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
2977
3050
|
const guard = new StallGuard();
|
|
2978
|
-
let [responseText, sessionId] = await this.runQuery(effectivePrompt, key, onText, model, profile, securityAnnotation, effectiveMaxTurns, projectOverride, onToolActivity, verboseLevel, abortController, guard, CHAT_TIMEOUT_MS, intent, turnPolicy);
|
|
3051
|
+
let [responseText, sessionId] = await this.runQuery(effectivePrompt, key, onText, model, profile, securityAnnotation, effectiveMaxTurns, projectOverride, onToolActivity, verboseLevel, abortController, guard, CHAT_TIMEOUT_MS, intent, turnPolicy, toolset);
|
|
2979
3052
|
// If we got a context-length / prompt-too-long error, retry with a fresh session
|
|
2980
3053
|
const errLower = responseText.toLowerCase();
|
|
2981
3054
|
const isContextOverflow = errLower.includes('prompt is too long') ||
|
|
@@ -2996,7 +3069,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2996
3069
|
`If this task involves pulling data for multiple entities, delegate each to a sub-agent using the Agent tool ` +
|
|
2997
3070
|
`instead of calling data-heavy tools directly.\n\n${text}`;
|
|
2998
3071
|
}
|
|
2999
|
-
[responseText, sessionId] = await this.runQuery(retryPrompt, key, onText, model, profile, securityAnnotation, maxTurns, undefined, onToolActivity, verboseLevel, abortController, undefined, CHAT_TIMEOUT_MS, intent, turnPolicy);
|
|
3072
|
+
[responseText, sessionId] = await this.runQuery(retryPrompt, key, onText, model, profile, securityAnnotation, maxTurns, undefined, onToolActivity, verboseLevel, abortController, undefined, CHAT_TIMEOUT_MS, intent, turnPolicy, toolset);
|
|
3000
3073
|
}
|
|
3001
3074
|
// Track exchange count, timestamp, and last exchange.
|
|
3002
3075
|
// Never store API error responses — they poison session history and create
|
|
@@ -3090,7 +3163,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3090
3163
|
// ── Run Query ─────────────────────────────────────────────────────
|
|
3091
3164
|
static RATE_LIMIT_MAX_RETRIES = 3;
|
|
3092
3165
|
static RATE_LIMIT_BACKOFF = [5000, 15000, 30000];
|
|
3093
|
-
async runQuery(prompt, sessionKey, onText, model, profile, securityAnnotation, maxTurnsOverride, projectOverride, onToolActivity, verboseLevel, abortController, stallGuard, timeoutMs, intentClassification, turnPolicy) {
|
|
3166
|
+
async runQuery(prompt, sessionKey, onText, model, profile, securityAnnotation, maxTurnsOverride, projectOverride, onToolActivity, verboseLevel, abortController, stallGuard, timeoutMs, intentClassification, turnPolicy, toolset = 'auto') {
|
|
3094
3167
|
// Parallelize context retrieval and project matching — they're independent
|
|
3095
3168
|
// If a project override is set, skip auto-matching entirely
|
|
3096
3169
|
const hasActiveSession = !!(sessionKey && this.sessions.has(sessionKey));
|
|
@@ -3197,6 +3270,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3197
3270
|
intentClassification,
|
|
3198
3271
|
turnPolicy: effectiveTurnPolicy,
|
|
3199
3272
|
effort: effectiveTurnPolicy?.effort ?? intentClassification?.suggestedEffort,
|
|
3273
|
+
toolset,
|
|
3200
3274
|
// Route destructive/admin/local write decisions from the direct user
|
|
3201
3275
|
// request only. Retrieved memory may still contribute integration
|
|
3202
3276
|
// continuity via contextRoutingText, but stale memories should not
|
|
@@ -3981,18 +4055,20 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3981
4055
|
*
|
|
3982
4056
|
* No LLM call — uses buildLocalSummary for instant summarization.
|
|
3983
4057
|
*/
|
|
3984
|
-
compactContext(sessionKey) {
|
|
3985
|
-
const summary = this.
|
|
4058
|
+
compactContext(sessionKey, reason = 'context_guard') {
|
|
4059
|
+
const summary = this.buildStructuredCompactionSummary(sessionKey);
|
|
3986
4060
|
if (!summary)
|
|
3987
|
-
return;
|
|
4061
|
+
return null;
|
|
3988
4062
|
// Build compaction block for working memory
|
|
3989
4063
|
const exchangeCount = this.exchangeCounts.get(sessionKey) ?? 0;
|
|
4064
|
+
const parentSessionId = this.sessions.get(sessionKey) ?? null;
|
|
3990
4065
|
const COMPACTION_START = '<!-- COMPACTION_START -->';
|
|
3991
4066
|
const COMPACTION_END = '<!-- COMPACTION_END -->';
|
|
3992
4067
|
const compactionBlock = [
|
|
3993
4068
|
COMPACTION_START,
|
|
3994
4069
|
`## Session Compaction (auto-generated)`,
|
|
3995
4070
|
`Session ${sessionKey} compacted at ${exchangeCount} exchanges.`,
|
|
4071
|
+
`Reason: ${reason}.`,
|
|
3996
4072
|
``,
|
|
3997
4073
|
summary,
|
|
3998
4074
|
``,
|
|
@@ -4030,6 +4106,20 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4030
4106
|
catch {
|
|
4031
4107
|
// If working memory write fails, still rotate — better than hitting the hard limit
|
|
4032
4108
|
}
|
|
4109
|
+
try {
|
|
4110
|
+
this.memoryStore?.saveSessionSummary?.(sessionKey, summary, exchangeCount);
|
|
4111
|
+
this.memoryStore?.recordSessionLineage?.({
|
|
4112
|
+
sessionKey,
|
|
4113
|
+
parentSessionId,
|
|
4114
|
+
childSessionId: null,
|
|
4115
|
+
reason,
|
|
4116
|
+
summary,
|
|
4117
|
+
exchangeCount,
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
catch {
|
|
4121
|
+
// Durable lineage is helpful, not required for compaction safety.
|
|
4122
|
+
}
|
|
4033
4123
|
// Rotate session — clear the session ID so next query starts fresh
|
|
4034
4124
|
// The working memory summary will provide continuity
|
|
4035
4125
|
this.sessions.delete(sessionKey);
|
|
@@ -4038,6 +4128,14 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4038
4128
|
this.sessionTimestamps.delete(sessionKey);
|
|
4039
4129
|
this.stallNudges.delete(sessionKey);
|
|
4040
4130
|
this.saveSessions();
|
|
4131
|
+
return summary;
|
|
4132
|
+
}
|
|
4133
|
+
compactSessionForGateway(sessionKey, reason = 'gateway_preflight') {
|
|
4134
|
+
const exchangeCount = this.exchangeCounts.get(sessionKey) ?? 0;
|
|
4135
|
+
const summary = this.compactContext(sessionKey, reason);
|
|
4136
|
+
return summary
|
|
4137
|
+
? { compacted: true, exchangeCount, summary, reason }
|
|
4138
|
+
: { compacted: false, exchangeCount, reason };
|
|
4041
4139
|
}
|
|
4042
4140
|
/**
|
|
4043
4141
|
* Expire sessions inactive for more than 24 hours.
|
|
@@ -4059,7 +4157,39 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4059
4157
|
* to avoid blocking the user's query.
|
|
4060
4158
|
*/
|
|
4061
4159
|
buildLocalSummary(sessionKey) {
|
|
4062
|
-
|
|
4160
|
+
let exchanges = this.lastExchanges.get(sessionKey) ?? [];
|
|
4161
|
+
if (exchanges.length === 0 && this.memoryStore && typeof this.memoryStore.getTranscriptTail === 'function') {
|
|
4162
|
+
try {
|
|
4163
|
+
const recent = this.memoryStore.getTranscriptTail(sessionKey, 0, SESSION_EXCHANGE_HISTORY_SIZE * 2);
|
|
4164
|
+
exchanges = this.pairTranscriptTurns(recent ?? []);
|
|
4165
|
+
}
|
|
4166
|
+
catch {
|
|
4167
|
+
exchanges = [];
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
return this.buildLocalSummaryFromTurns(exchanges);
|
|
4171
|
+
}
|
|
4172
|
+
buildStructuredCompactionSummary(sessionKey) {
|
|
4173
|
+
const exchanges = this.lastExchanges.get(sessionKey) ?? [];
|
|
4174
|
+
const summary = this.buildLocalSummary(sessionKey);
|
|
4175
|
+
if (!summary)
|
|
4176
|
+
return '';
|
|
4177
|
+
const latest = exchanges.at(-1);
|
|
4178
|
+
const lastUser = latest?.user
|
|
4179
|
+
? latest.user.slice(0, 400).replace(/\s+/g, ' ')
|
|
4180
|
+
: '';
|
|
4181
|
+
const continuity = [
|
|
4182
|
+
'- Exact details remain in transcripts; use transcript_search before relying on this handoff for names, dates, IDs, files, or sent-message status.',
|
|
4183
|
+
'- Keep tool outputs bounded and prefer targeted reads over full log dumps.',
|
|
4184
|
+
lastUser ? `- Last visible user request: ${lastUser}` : '',
|
|
4185
|
+
].filter(Boolean);
|
|
4186
|
+
return [
|
|
4187
|
+
'### Recent Conversation',
|
|
4188
|
+
summary,
|
|
4189
|
+
'',
|
|
4190
|
+
'### Continuity Notes',
|
|
4191
|
+
continuity.join('\n'),
|
|
4192
|
+
].join('\n');
|
|
4063
4193
|
}
|
|
4064
4194
|
buildLocalSummaryFromTurns(turns, opts) {
|
|
4065
4195
|
if (turns.length === 0)
|
|
@@ -4971,13 +5101,17 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4971
5101
|
const progress = JSON.parse(fs.readFileSync(progressFile, 'utf-8'));
|
|
4972
5102
|
const parts = [`## Previous Progress (run #${progress.runCount}, ${progress.lastRunAt})`];
|
|
4973
5103
|
if (progress.completedItems?.length > 0) {
|
|
4974
|
-
parts.push(`Completed: ${progress.completedItems.slice(-10).join(', ')}`);
|
|
5104
|
+
parts.push(`Completed: ${progress.completedItems.slice(-10).map(capContextItem).join(', ')}`);
|
|
4975
5105
|
}
|
|
4976
5106
|
if (progress.pendingItems?.length > 0) {
|
|
4977
|
-
|
|
5107
|
+
const pendingItems = progress.pendingItems.slice(0, CRON_PROGRESS_PENDING_MAX_ITEMS).map(capContextItem);
|
|
5108
|
+
const suffix = progress.pendingItems.length > CRON_PROGRESS_PENDING_MAX_ITEMS
|
|
5109
|
+
? ` (${progress.pendingItems.length - CRON_PROGRESS_PENDING_MAX_ITEMS} more omitted)`
|
|
5110
|
+
: '';
|
|
5111
|
+
parts.push(`Pending: ${pendingItems.join(', ')}${suffix}`);
|
|
4978
5112
|
}
|
|
4979
5113
|
if (progress.notes) {
|
|
4980
|
-
parts.push(`Notes: ${progress.notes}`);
|
|
5114
|
+
parts.push(`Notes: ${capContextBlock(progress.notes, CRON_PROGRESS_NOTES_MAX_CHARS)}`);
|
|
4981
5115
|
}
|
|
4982
5116
|
progressContext = parts.join('\n') + '\n\n' +
|
|
4983
5117
|
'Continue from where you left off. Use `cron_progress_write` at the end to save what you completed and what\'s pending.\n\n';
|
|
@@ -5999,8 +6133,8 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
5999
6133
|
* so follow-up conversation has context.
|
|
6000
6134
|
*/
|
|
6001
6135
|
injectContext(sessionKey, userText, assistantText) {
|
|
6002
|
-
const trimmedUser = userText
|
|
6003
|
-
const trimmedAssistant = assistantText
|
|
6136
|
+
const trimmedUser = capContextBlock(userText, INJECTED_CONTEXT_MAX_CHARS);
|
|
6137
|
+
const trimmedAssistant = capContextBlock(assistantText, INJECTED_CONTEXT_MAX_CHARS);
|
|
6004
6138
|
// Add to in-memory exchange history
|
|
6005
6139
|
const history = this.lastExchanges.get(sessionKey) ?? [];
|
|
6006
6140
|
history.push({ user: trimmedUser, assistant: trimmedAssistant });
|