clementine-agent 1.18.19 → 1.18.21
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 +14 -0
- package/dist/agent/assistant.js +190 -35
- 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 +112 -6
- 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 +8 -1
- package/dist/gateway/router.js +326 -12
- 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/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;
|
|
@@ -343,6 +370,15 @@ function resultInputTokens(result) {
|
|
|
343
370
|
export function looksLikeOneMillionContextError(value) {
|
|
344
371
|
return looksLikeClaudeOneMillionContextError(value);
|
|
345
372
|
}
|
|
373
|
+
export function oneMillionContextRecoveryMessage() {
|
|
374
|
+
return "Claude rejected 1M context for this account. I've switched Clementine to persistent 200K recovery mode and reset the session. Restart Clementine once so every background worker starts with the same safe setting.";
|
|
375
|
+
}
|
|
376
|
+
export function looksLikeProviderApiErrorResponse(value) {
|
|
377
|
+
const text = String(value ?? '').trim();
|
|
378
|
+
return /^api error:/i.test(text)
|
|
379
|
+
|| /^error:\s*api error:/i.test(text)
|
|
380
|
+
|| looksLikeOneMillionContextError(text);
|
|
381
|
+
}
|
|
346
382
|
export function looksLikeNoResponseRequested(value) {
|
|
347
383
|
const text = String(value ?? '').trim();
|
|
348
384
|
return /^no response requested\.?$/i.test(text);
|
|
@@ -1310,6 +1346,10 @@ export class PersonalAssistant {
|
|
|
1310
1346
|
getExchangeCount(sessionKey) {
|
|
1311
1347
|
return this.exchangeCounts.get(sessionKey) ?? 0;
|
|
1312
1348
|
}
|
|
1349
|
+
hasRecentApprovalPrompt(sessionKey) {
|
|
1350
|
+
const lastAssistant = this.lastExchanges.get(sessionKey)?.at(-1)?.assistant ?? '';
|
|
1351
|
+
return looksLikeApprovalPrompt(lastAssistant);
|
|
1352
|
+
}
|
|
1313
1353
|
getMemoryChunkCount() {
|
|
1314
1354
|
if (!this.memoryStore)
|
|
1315
1355
|
return 0;
|
|
@@ -1941,7 +1981,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1941
1981
|
}
|
|
1942
1982
|
// ── Build SDK Options ─────────────────────────────────────────────
|
|
1943
1983
|
async buildOptions(opts = {}) {
|
|
1944
|
-
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;
|
|
1945
1985
|
const isCron = cronTier !== null;
|
|
1946
1986
|
const toolsDisabledForCall = disableAllTools || (isHeartbeat && !isCron);
|
|
1947
1987
|
const promptScopeText = toolScopeText ?? '';
|
|
@@ -1992,7 +2032,27 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1992
2032
|
const safeContextToolRoute = allowContextToolRoute && !contextToolRoute.fullSurface
|
|
1993
2033
|
? contextToolRoute
|
|
1994
2034
|
: emptyToolRoute();
|
|
1995
|
-
|
|
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
|
+
}
|
|
1996
2056
|
let allowedTools = [];
|
|
1997
2057
|
const addAllowed = (...tools) => {
|
|
1998
2058
|
for (const tool of tools) {
|
|
@@ -2012,9 +2072,13 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2012
2072
|
const memoryNeeded = autonomousToolRun
|
|
2013
2073
|
|| retrievalContext.trim().length > 0
|
|
2014
2074
|
|| (turnPolicy?.retrievalTier !== undefined && turnPolicy.retrievalTier !== 'none');
|
|
2015
|
-
const localReadNeeded = taskIntent || /\b(repo|repository|code|file|files|folder|directory|path|log|logs|config|read|show|grep|diff|search)\b/i.test(promptScopeLower);
|
|
2016
|
-
const
|
|
2017
|
-
|
|
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));
|
|
2018
2082
|
if (!toolsDisabledForCall) {
|
|
2019
2083
|
if (toolRoute.fullSurface) {
|
|
2020
2084
|
addAllowed('Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'WebSearch', 'WebFetch');
|
|
@@ -2023,8 +2087,12 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2023
2087
|
else {
|
|
2024
2088
|
if (localReadNeeded)
|
|
2025
2089
|
addAllowed('Read', 'Glob', 'Grep');
|
|
2026
|
-
if (localWriteNeeded)
|
|
2027
|
-
|
|
2090
|
+
if (localWriteNeeded) {
|
|
2091
|
+
if (toolset === 'diagnostic')
|
|
2092
|
+
addAllowed('Bash');
|
|
2093
|
+
else
|
|
2094
|
+
addAllowed('Write', 'Edit', 'Bash');
|
|
2095
|
+
}
|
|
2028
2096
|
if (toolRoute.bundles.includes('web_research') || toolRoute.bundles.includes('docs_lookup')) {
|
|
2029
2097
|
addAllowed('WebSearch', 'WebFetch');
|
|
2030
2098
|
}
|
|
@@ -2032,7 +2100,12 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2032
2100
|
addClementineTools(CLEMENTINE_CORE_TOOL_NAMES);
|
|
2033
2101
|
addClementineTools(CLEMENTINE_RELATIONSHIP_TOOL_NAMES);
|
|
2034
2102
|
}
|
|
2035
|
-
|
|
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) {
|
|
2036
2109
|
addClementineTools(CLEMENTINE_MEMORY_WRITE_TOOL_NAMES);
|
|
2037
2110
|
addClementineTools(CLEMENTINE_WORKSPACE_TOOL_NAMES);
|
|
2038
2111
|
}
|
|
@@ -2049,20 +2122,22 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2049
2122
|
addClementineTools(CLEMENTINE_INTEGRATION_TOOL_NAMES);
|
|
2050
2123
|
addClementineTools(CLEMENTINE_ADMIN_TOOL_NAMES);
|
|
2051
2124
|
}
|
|
2052
|
-
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))) {
|
|
2053
2127
|
addClementineTools(CLEMENTINE_COMM_TOOL_NAMES);
|
|
2054
2128
|
}
|
|
2055
|
-
if (
|
|
2129
|
+
if ((toolset === 'auto' || toolset === 'full')
|
|
2130
|
+
&& (toolRoute.bundles.includes('github') || toolRoute.bundles.includes('browser') || toolRoute.bundles.includes('web_research'))) {
|
|
2056
2131
|
addClementineTools(CLEMENTINE_RESEARCH_TOOL_NAMES);
|
|
2057
2132
|
}
|
|
2058
|
-
if (enableTeams) {
|
|
2133
|
+
if (enableTeams && (toolset === 'auto' || toolset === 'full')) {
|
|
2059
2134
|
addAllowed('Task', 'Agent');
|
|
2060
2135
|
addClementineTools(CLEMENTINE_TEAM_TOOL_NAMES);
|
|
2061
2136
|
addClementineTools(CLEMENTINE_JOB_TOOL_NAMES);
|
|
2062
2137
|
}
|
|
2063
2138
|
}
|
|
2064
2139
|
// Include local user scripts/plugins for task-like or explicit full-surface turns.
|
|
2065
|
-
if (taskIntent || toolRoute.fullSurface || adminNeeded) {
|
|
2140
|
+
if (toolsetAllowsLocalWrites(toolset) && (taskIntent || toolRoute.fullSurface || adminNeeded)) {
|
|
2066
2141
|
try {
|
|
2067
2142
|
const toolsDir = path.join(BASE_DIR, 'tools');
|
|
2068
2143
|
const pluginsDir = path.join(BASE_DIR, 'plugins');
|
|
@@ -2405,6 +2480,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2405
2480
|
isolateClaudeConfig,
|
|
2406
2481
|
inheritFullClaudeEnv: shouldInheritClaudeEnv,
|
|
2407
2482
|
maxBudgetUsd: enforcedBudget,
|
|
2483
|
+
toolset,
|
|
2408
2484
|
isCron,
|
|
2409
2485
|
cronTier,
|
|
2410
2486
|
isPlanStep,
|
|
@@ -2797,6 +2873,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2797
2873
|
const projectOverride = options?.projectOverride;
|
|
2798
2874
|
const verboseLevel = options?.verboseLevel;
|
|
2799
2875
|
const abortController = options?.abortController;
|
|
2876
|
+
const toolset = options?.toolset ?? 'auto';
|
|
2800
2877
|
const key = sessionKey ?? undefined;
|
|
2801
2878
|
this._lastUserMessage = text;
|
|
2802
2879
|
let sessionRotated = false;
|
|
@@ -2897,11 +2974,14 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2897
2974
|
const exchanges = this.lastExchanges.get(key) ?? [];
|
|
2898
2975
|
if (exchanges.length === 0 && this.memoryStore) {
|
|
2899
2976
|
try {
|
|
2900
|
-
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);
|
|
2901
2980
|
if (recentSummaries.length > 0) {
|
|
2902
2981
|
const last = recentSummaries[0];
|
|
2903
|
-
const
|
|
2904
|
-
|
|
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
|
|
2905
2985
|
const ago = formatTimeAgo(ageMs);
|
|
2906
2986
|
effectivePrompt =
|
|
2907
2987
|
`[Last conversation (${ago}):\n${last.summary.slice(0, 600)}]\n\n` +
|
|
@@ -2937,7 +3017,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2937
3017
|
if (allPending.length > 0) {
|
|
2938
3018
|
const contextLines = [];
|
|
2939
3019
|
for (const ctx of allPending) {
|
|
2940
|
-
|
|
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}`);
|
|
2941
3023
|
}
|
|
2942
3024
|
effectivePrompt =
|
|
2943
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}`;
|
|
@@ -2966,7 +3048,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2966
3048
|
const effectiveMaxTurns = maxTurns ?? turnPolicy.maxTurns;
|
|
2967
3049
|
const CHAT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
2968
3050
|
const guard = new StallGuard();
|
|
2969
|
-
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);
|
|
2970
3052
|
// If we got a context-length / prompt-too-long error, retry with a fresh session
|
|
2971
3053
|
const errLower = responseText.toLowerCase();
|
|
2972
3054
|
const isContextOverflow = errLower.includes('prompt is too long') ||
|
|
@@ -2987,12 +3069,12 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2987
3069
|
`If this task involves pulling data for multiple entities, delegate each to a sub-agent using the Agent tool ` +
|
|
2988
3070
|
`instead of calling data-heavy tools directly.\n\n${text}`;
|
|
2989
3071
|
}
|
|
2990
|
-
[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);
|
|
2991
3073
|
}
|
|
2992
3074
|
// Track exchange count, timestamp, and last exchange.
|
|
2993
3075
|
// Never store API error responses — they poison session history and create
|
|
2994
3076
|
// a self-reinforcing loop where every subsequent request replays the errors.
|
|
2995
|
-
const isApiError =
|
|
3077
|
+
const isApiError = looksLikeProviderApiErrorResponse(responseText);
|
|
2996
3078
|
if (key && !isApiError) {
|
|
2997
3079
|
this.exchangeCounts.set(key, (this.exchangeCounts.get(key) ?? 0) + 1);
|
|
2998
3080
|
this.sessionTimestamps.set(key, new Date());
|
|
@@ -3081,7 +3163,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3081
3163
|
// ── Run Query ─────────────────────────────────────────────────────
|
|
3082
3164
|
static RATE_LIMIT_MAX_RETRIES = 3;
|
|
3083
3165
|
static RATE_LIMIT_BACKOFF = [5000, 15000, 30000];
|
|
3084
|
-
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') {
|
|
3085
3167
|
// Parallelize context retrieval and project matching — they're independent
|
|
3086
3168
|
// If a project override is set, skip auto-matching entirely
|
|
3087
3169
|
const hasActiveSession = !!(sessionKey && this.sessions.has(sessionKey));
|
|
@@ -3188,6 +3270,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3188
3270
|
intentClassification,
|
|
3189
3271
|
turnPolicy: effectiveTurnPolicy,
|
|
3190
3272
|
effort: effectiveTurnPolicy?.effort ?? intentClassification?.suggestedEffort,
|
|
3273
|
+
toolset,
|
|
3191
3274
|
// Route destructive/admin/local write decisions from the direct user
|
|
3192
3275
|
// request only. Retrieved memory may still contribute integration
|
|
3193
3276
|
// continuity via contextRoutingText, but stale memories should not
|
|
@@ -3432,7 +3515,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3432
3515
|
this.exchangeCounts.set(sessionKey, 0);
|
|
3433
3516
|
this._compactedSessions.delete(sessionKey);
|
|
3434
3517
|
}
|
|
3435
|
-
responseText = responseText || (
|
|
3518
|
+
responseText = responseText || (oneMillionContextRecoveryMessage());
|
|
3436
3519
|
}
|
|
3437
3520
|
else if (lower.includes('rate') && lower.includes('limit')) {
|
|
3438
3521
|
hitRateLimit = true;
|
|
@@ -3485,7 +3568,19 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3485
3568
|
else if ('result' in result && result.result) {
|
|
3486
3569
|
// Success: use SDK result text if streaming didn't capture a substantive response
|
|
3487
3570
|
const sdkResult = result.result;
|
|
3488
|
-
if (
|
|
3571
|
+
if (looksLikeOneMillionContextError(sdkResult)) {
|
|
3572
|
+
logger.warn({ sessionKey }, '1M context error surfaced as SDK result text — forcing recovery');
|
|
3573
|
+
applyOneMillionContextRecovery();
|
|
3574
|
+
if (sessionKey) {
|
|
3575
|
+
this.sessions.delete(sessionKey);
|
|
3576
|
+
this.exchangeCounts.set(sessionKey, 0);
|
|
3577
|
+
this._compactedSessions.delete(sessionKey);
|
|
3578
|
+
}
|
|
3579
|
+
responseText = oneMillionContextRecoveryMessage();
|
|
3580
|
+
if (onText)
|
|
3581
|
+
await onText(responseText);
|
|
3582
|
+
}
|
|
3583
|
+
else if (looksLikeContextThrashText(sdkResult)) {
|
|
3489
3584
|
logger.warn({ sessionKey }, 'Autocompact thrashing surfaced as SDK result text — rotating session');
|
|
3490
3585
|
preRotationSnapshot = {
|
|
3491
3586
|
toolCalls: stallGuard?.getToolCalls() ?? [],
|
|
@@ -3563,7 +3658,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3563
3658
|
this.exchangeCounts.set(sessionKey, 0);
|
|
3564
3659
|
this._compactedSessions.delete(sessionKey);
|
|
3565
3660
|
}
|
|
3566
|
-
responseText = responseText || (
|
|
3661
|
+
responseText = responseText || (oneMillionContextRecoveryMessage());
|
|
3567
3662
|
}
|
|
3568
3663
|
else if (errStr.includes('rate') && (errStr.includes('limit') || errStr.includes('rate_limit'))) {
|
|
3569
3664
|
hitRateLimit = true;
|
|
@@ -3960,18 +4055,20 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3960
4055
|
*
|
|
3961
4056
|
* No LLM call — uses buildLocalSummary for instant summarization.
|
|
3962
4057
|
*/
|
|
3963
|
-
compactContext(sessionKey) {
|
|
3964
|
-
const summary = this.
|
|
4058
|
+
compactContext(sessionKey, reason = 'context_guard') {
|
|
4059
|
+
const summary = this.buildStructuredCompactionSummary(sessionKey);
|
|
3965
4060
|
if (!summary)
|
|
3966
|
-
return;
|
|
4061
|
+
return null;
|
|
3967
4062
|
// Build compaction block for working memory
|
|
3968
4063
|
const exchangeCount = this.exchangeCounts.get(sessionKey) ?? 0;
|
|
4064
|
+
const parentSessionId = this.sessions.get(sessionKey) ?? null;
|
|
3969
4065
|
const COMPACTION_START = '<!-- COMPACTION_START -->';
|
|
3970
4066
|
const COMPACTION_END = '<!-- COMPACTION_END -->';
|
|
3971
4067
|
const compactionBlock = [
|
|
3972
4068
|
COMPACTION_START,
|
|
3973
4069
|
`## Session Compaction (auto-generated)`,
|
|
3974
4070
|
`Session ${sessionKey} compacted at ${exchangeCount} exchanges.`,
|
|
4071
|
+
`Reason: ${reason}.`,
|
|
3975
4072
|
``,
|
|
3976
4073
|
summary,
|
|
3977
4074
|
``,
|
|
@@ -4009,6 +4106,20 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4009
4106
|
catch {
|
|
4010
4107
|
// If working memory write fails, still rotate — better than hitting the hard limit
|
|
4011
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
|
+
}
|
|
4012
4123
|
// Rotate session — clear the session ID so next query starts fresh
|
|
4013
4124
|
// The working memory summary will provide continuity
|
|
4014
4125
|
this.sessions.delete(sessionKey);
|
|
@@ -4017,6 +4128,14 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4017
4128
|
this.sessionTimestamps.delete(sessionKey);
|
|
4018
4129
|
this.stallNudges.delete(sessionKey);
|
|
4019
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 };
|
|
4020
4139
|
}
|
|
4021
4140
|
/**
|
|
4022
4141
|
* Expire sessions inactive for more than 24 hours.
|
|
@@ -4038,7 +4157,39 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4038
4157
|
* to avoid blocking the user's query.
|
|
4039
4158
|
*/
|
|
4040
4159
|
buildLocalSummary(sessionKey) {
|
|
4041
|
-
|
|
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');
|
|
4042
4193
|
}
|
|
4043
4194
|
buildLocalSummaryFromTurns(turns, opts) {
|
|
4044
4195
|
if (turns.length === 0)
|
|
@@ -4950,13 +5101,17 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4950
5101
|
const progress = JSON.parse(fs.readFileSync(progressFile, 'utf-8'));
|
|
4951
5102
|
const parts = [`## Previous Progress (run #${progress.runCount}, ${progress.lastRunAt})`];
|
|
4952
5103
|
if (progress.completedItems?.length > 0) {
|
|
4953
|
-
parts.push(`Completed: ${progress.completedItems.slice(-10).join(', ')}`);
|
|
5104
|
+
parts.push(`Completed: ${progress.completedItems.slice(-10).map(capContextItem).join(', ')}`);
|
|
4954
5105
|
}
|
|
4955
5106
|
if (progress.pendingItems?.length > 0) {
|
|
4956
|
-
|
|
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}`);
|
|
4957
5112
|
}
|
|
4958
5113
|
if (progress.notes) {
|
|
4959
|
-
parts.push(`Notes: ${progress.notes}`);
|
|
5114
|
+
parts.push(`Notes: ${capContextBlock(progress.notes, CRON_PROGRESS_NOTES_MAX_CHARS)}`);
|
|
4960
5115
|
}
|
|
4961
5116
|
progressContext = parts.join('\n') + '\n\n' +
|
|
4962
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';
|
|
@@ -5978,8 +6133,8 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
5978
6133
|
* so follow-up conversation has context.
|
|
5979
6134
|
*/
|
|
5980
6135
|
injectContext(sessionKey, userText, assistantText) {
|
|
5981
|
-
const trimmedUser = userText
|
|
5982
|
-
const trimmedAssistant = assistantText
|
|
6136
|
+
const trimmedUser = capContextBlock(userText, INJECTED_CONTEXT_MAX_CHARS);
|
|
6137
|
+
const trimmedAssistant = capContextBlock(assistantText, INJECTED_CONTEXT_MAX_CHARS);
|
|
5983
6138
|
// Add to in-memory exchange history
|
|
5984
6139
|
const history = this.lastExchanges.get(sessionKey) ?? [];
|
|
5985
6140
|
history.push({ user: trimmedUser, assistant: trimmedAssistant });
|
|
@@ -5,14 +5,41 @@
|
|
|
5
5
|
* Source modifications from self-improve are tracked in ~/.clementine/ (not git),
|
|
6
6
|
* so git pull is always clean. After pulling, source mods are reconciled.
|
|
7
7
|
*/
|
|
8
|
-
import { execSync } from 'node:child_process';
|
|
9
|
-
import { writeFileSync } from 'node:fs';
|
|
8
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import pino from 'pino';
|
|
12
12
|
import { BASE_DIR } from '../config.js';
|
|
13
13
|
import { reconcileSourceMods } from './source-mods.js';
|
|
14
14
|
const logger = pino({ name: 'clementine.auto-update' });
|
|
15
15
|
const SENTINEL_PATH = path.join(BASE_DIR, '.restart-sentinel.json');
|
|
16
|
+
function readDataEnv() {
|
|
17
|
+
const envPath = path.join(BASE_DIR, '.env');
|
|
18
|
+
if (!existsSync(envPath))
|
|
19
|
+
return {};
|
|
20
|
+
try {
|
|
21
|
+
return Object.fromEntries(readFileSync(envPath, 'utf-8')
|
|
22
|
+
.split(/\r?\n/)
|
|
23
|
+
.map((line) => line.trim())
|
|
24
|
+
.filter((line) => line && !line.startsWith('#') && line.includes('='))
|
|
25
|
+
.map((line) => {
|
|
26
|
+
const idx = line.indexOf('=');
|
|
27
|
+
return [line.slice(0, idx).trim(), line.slice(idx + 1).trim().replace(/^["']|["']$/g, '')];
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function flagEnabled(name, envFile) {
|
|
35
|
+
const raw = process.env[name] ?? envFile[name];
|
|
36
|
+
return /^(1|true|yes|on)$/i.test(String(raw ?? ''));
|
|
37
|
+
}
|
|
38
|
+
function shouldPrefetchEmbeddings() {
|
|
39
|
+
const envFile = readDataEnv();
|
|
40
|
+
return flagEnabled('CLEMENTINE_INSTALL_EMBEDDINGS', envFile)
|
|
41
|
+
|| flagEnabled('CLEMENTINE_PREFETCH_EMBEDDINGS', envFile);
|
|
42
|
+
}
|
|
16
43
|
/**
|
|
17
44
|
* Check if upstream has new commits. Safe to call from cron — no side effects.
|
|
18
45
|
*/
|
|
@@ -121,6 +148,23 @@ export async function applyUpdate(pkgDir) {
|
|
|
121
148
|
logger.error({ err }, 'Build failed after update');
|
|
122
149
|
return { success: false, error: `Build failed after update: ${String(err)}` };
|
|
123
150
|
}
|
|
151
|
+
// 4b. Optional embedding model prefetch. npm postinstall may run before
|
|
152
|
+
// the freshly pulled TypeScript has been built; this second pass uses the
|
|
153
|
+
// just-built CLI so repo updates and npm-style updates behave the same.
|
|
154
|
+
if (shouldPrefetchEmbeddings()) {
|
|
155
|
+
try {
|
|
156
|
+
execFileSync(process.execPath, [path.join(pkgDir, 'dist', 'cli', 'index.js'), 'memory', 'model', 'install'], {
|
|
157
|
+
cwd: pkgDir,
|
|
158
|
+
stdio: 'pipe',
|
|
159
|
+
env: { ...process.env, CLEMENTINE_HOME: BASE_DIR },
|
|
160
|
+
timeout: 10 * 60_000,
|
|
161
|
+
});
|
|
162
|
+
logger.info('Local embedding model prefetch succeeded after update');
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
logger.warn({ err }, 'Local embedding model prefetch failed after update');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
124
168
|
// 5. Reconcile source modifications
|
|
125
169
|
const reconcileResult = reconcileSourceMods(pkgDir);
|
|
126
170
|
logger.info({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClementineJson } from '../config/clementine-json.js';
|
|
2
|
+
import { type ToolsetName } from './toolsets.js';
|
|
2
3
|
export type ProactivityMode = 'quiet' | 'balanced' | 'proactive' | 'operator';
|
|
3
4
|
export type ResponseStyle = 'concise' | 'balanced' | 'detailed';
|
|
4
5
|
export type ProgressVisibility = 'quiet' | 'normal' | 'detailed';
|
|
@@ -19,14 +20,29 @@ export type LocalTurnIntent = {
|
|
|
19
20
|
kind: 'stop';
|
|
20
21
|
} | {
|
|
21
22
|
kind: 'status';
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'last_action';
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'compress_context';
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'debug_status';
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'toolset';
|
|
31
|
+
toolset: ToolsetName;
|
|
22
32
|
} | {
|
|
23
33
|
kind: 'preference_update';
|
|
24
34
|
updates: AssistantExperienceUpdate;
|
|
25
35
|
summary: string;
|
|
26
36
|
};
|
|
37
|
+
export type ApprovalReply = true | false | 'always' | null;
|
|
27
38
|
export declare function isStopRequest(text: string): boolean;
|
|
28
39
|
export declare function isStatusRequest(text: string): boolean;
|
|
40
|
+
export declare function isLastActionRequest(text: string): boolean;
|
|
41
|
+
export declare function isCompressContextRequest(text: string): boolean;
|
|
42
|
+
export declare function isDebugStatusRequest(text: string): boolean;
|
|
29
43
|
export declare function isTinyAcknowledgment(text: string): boolean;
|
|
44
|
+
export declare function detectApprovalReply(text: string): ApprovalReply;
|
|
45
|
+
export declare function looksLikeApprovalPrompt(text: string): boolean;
|
|
30
46
|
export declare function detectLocalTurn(text: string): LocalTurnIntent;
|
|
31
47
|
export declare function applyAssistantExperienceUpdate(cfg: ClementineJson, updates: AssistantExperienceUpdate): ClementineJson;
|
|
32
48
|
//# sourceMappingURL=local-turn.d.ts.map
|
package/dist/agent/local-turn.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { isStandaloneGreeting } from './turn-policy.js';
|
|
2
|
+
import { normalizeToolsetName } from './toolsets.js';
|
|
2
3
|
function normalize(text) {
|
|
3
4
|
return text
|
|
4
5
|
.trim()
|
|
5
6
|
.toLowerCase()
|
|
7
|
+
.replace(/[‘’`]/g, "'")
|
|
6
8
|
.replace(/[.!?]+$/g, '')
|
|
7
9
|
.replace(/\s+/g, ' ');
|
|
8
10
|
}
|
|
@@ -20,7 +22,31 @@ export function isStatusRequest(text) {
|
|
|
20
22
|
const n = normalize(text);
|
|
21
23
|
if (wordCount(n) > 8)
|
|
22
24
|
return false;
|
|
23
|
-
return /^(status|task status|deep status|progress|what'?s happening|what'?s going on|what are you doing|are you working|anything running|what'?s
|
|
25
|
+
return /^(status|task status|deep status|progress|what'?s happening|what'?s going on|what are you doing|what are you working on|what are you running|are you working|anything running|what'?s runnin?g?(?: now| right now)?|what is runnin?g?(?: now| right now)?|background status|check status|where are we)$/.test(n);
|
|
26
|
+
}
|
|
27
|
+
export function isLastActionRequest(text) {
|
|
28
|
+
const n = normalize(text);
|
|
29
|
+
if (wordCount(n) > 10)
|
|
30
|
+
return false;
|
|
31
|
+
return /^(last action|last turn|what happened last turn|what did you do|did you do it|did that actually run|did you actually do it|why didn'?t you do it|why did that not run|what happened)$/.test(n);
|
|
32
|
+
}
|
|
33
|
+
export function isCompressContextRequest(text) {
|
|
34
|
+
const n = normalize(text);
|
|
35
|
+
if (wordCount(n) > 8)
|
|
36
|
+
return false;
|
|
37
|
+
return /^(compress context|compact context|compress session|compact session|context compact|context compress|save and reset context|reset context but keep memory)$/.test(n);
|
|
38
|
+
}
|
|
39
|
+
export function isDebugStatusRequest(text) {
|
|
40
|
+
const n = normalize(text);
|
|
41
|
+
if (wordCount(n) > 6)
|
|
42
|
+
return false;
|
|
43
|
+
return /^(debug|debug status|session debug|agent debug|diagnostics|show diagnostics)$/.test(n);
|
|
44
|
+
}
|
|
45
|
+
function parseToolsetRequest(text) {
|
|
46
|
+
const n = normalize(text);
|
|
47
|
+
const match = n.match(/^(?:set |switch |use |enable )?(?:toolset|tool set|tools mode|tool mode)(?: to|:)? ([a-z _-]+)$/)
|
|
48
|
+
?? n.match(/^toolset ([a-z _-]+)$/);
|
|
49
|
+
return match ? normalizeToolsetName(match[1]) : null;
|
|
24
50
|
}
|
|
25
51
|
export function isTinyAcknowledgment(text) {
|
|
26
52
|
const n = normalize(text);
|
|
@@ -28,6 +54,24 @@ export function isTinyAcknowledgment(text) {
|
|
|
28
54
|
return false;
|
|
29
55
|
return /^(thanks|thank you|thx|ty|nice|great|perfect|awesome|cool|ok|okay|sounds good|got it|makes sense|love it)$/.test(n);
|
|
30
56
|
}
|
|
57
|
+
export function detectApprovalReply(text) {
|
|
58
|
+
const n = normalize(text);
|
|
59
|
+
if (wordCount(n) > 4)
|
|
60
|
+
return null;
|
|
61
|
+
if (/^(always)$/.test(n))
|
|
62
|
+
return 'always';
|
|
63
|
+
if (/^(no|nope|deny|denied|skip)$/.test(n))
|
|
64
|
+
return false;
|
|
65
|
+
if (/^(yes|y|yep|yeah|ok|okay|approve|approved|go|go ahead|do it|send it|perfect|sounds good|looks good|lgtm)$/.test(n)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
export function looksLikeApprovalPrompt(text) {
|
|
71
|
+
const n = normalize(text);
|
|
72
|
+
return /\b(good to go|okay to send|ok to send|ready to send|should i send|want me to send|approve|confirm|fire it off)\b/.test(n)
|
|
73
|
+
|| /\b(send|email|message|post|publish|delete|change|update|run|execute)\b[\s\S]{0,120}\?$/i.test(text.trim());
|
|
74
|
+
}
|
|
31
75
|
function parseProactivity(text) {
|
|
32
76
|
if (/\b(operator mode|operator)\b/i.test(text))
|
|
33
77
|
return 'operator';
|
|
@@ -71,6 +115,15 @@ export function detectLocalTurn(text) {
|
|
|
71
115
|
return { kind: 'stop' };
|
|
72
116
|
if (isStatusRequest(text))
|
|
73
117
|
return { kind: 'status' };
|
|
118
|
+
if (isLastActionRequest(text))
|
|
119
|
+
return { kind: 'last_action' };
|
|
120
|
+
if (isCompressContextRequest(text))
|
|
121
|
+
return { kind: 'compress_context' };
|
|
122
|
+
if (isDebugStatusRequest(text))
|
|
123
|
+
return { kind: 'debug_status' };
|
|
124
|
+
const toolset = parseToolsetRequest(text);
|
|
125
|
+
if (toolset)
|
|
126
|
+
return { kind: 'toolset', toolset };
|
|
74
127
|
if (isStandaloneGreeting(text))
|
|
75
128
|
return { kind: 'greeting' };
|
|
76
129
|
if (isTinyAcknowledgment(text))
|