@wangzhizhi/remi 0.1.225 → 0.1.244

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.
Files changed (36) hide show
  1. package/README.md +206 -2
  2. package/dist/help.js +1 -1
  3. package/dist/initPrompt.js +1 -1
  4. package/dist/setup.js +5 -4
  5. package/dist/statusCard.js +1 -1
  6. package/dist/statusline.js +1 -1
  7. package/dist/tui/RemiApp.js +55 -7
  8. package/dist/tui/hooksPanel.js +2 -2
  9. package/dist/tui/renderers/MessageList.js +10 -7
  10. package/dist/version.js +1 -1
  11. package/node_modules/@remi/compact/dist/index.js +13 -5
  12. package/node_modules/@remi/compact/package.json +1 -1
  13. package/node_modules/@remi/config/dist/index.js +816 -38
  14. package/node_modules/@remi/config/package.json +1 -1
  15. package/node_modules/@remi/core/dist/cacheBenchmark.js +217 -0
  16. package/node_modules/@remi/core/dist/cacheDiagnostics.js +300 -0
  17. package/node_modules/@remi/core/dist/contextBuilder.js +13 -11
  18. package/node_modules/@remi/core/dist/index.js +630 -33
  19. package/node_modules/@remi/core/dist/stableHash.js +30 -0
  20. package/node_modules/@remi/core/package.json +1 -1
  21. package/node_modules/@remi/hooks/package.json +1 -1
  22. package/node_modules/@remi/llm/package.json +1 -1
  23. package/node_modules/@remi/mcp/dist/index.js +32 -18
  24. package/node_modules/@remi/mcp/package.json +1 -1
  25. package/node_modules/@remi/memory/package.json +1 -1
  26. package/node_modules/@remi/permissions/package.json +1 -1
  27. package/node_modules/@remi/sessions/dist/index.js +38 -0
  28. package/node_modules/@remi/sessions/package.json +1 -1
  29. package/node_modules/@remi/skills/dist/index.js +2 -2
  30. package/node_modules/@remi/skills/package.json +1 -1
  31. package/node_modules/@remi/terminal-markdown/dist/index.js +159 -22
  32. package/node_modules/@remi/terminal-markdown/package.json +1 -1
  33. package/node_modules/@remi/tools/dist/index.js +1 -1
  34. package/node_modules/@remi/tools/package.json +1 -1
  35. package/package.json +13 -13
  36. package/prompt/tool-use-system.md +19 -3
@@ -1,19 +1,23 @@
1
1
  import { loadRemiConfig, normalizePermissionProfile, resolveToolPermissionMode, } from '@remi/config';
2
2
  import { buildBaseSystemPrompt, buildCurrentTurnSystemPrompt, defaultResponseStyleId, resolveResponseStyle, } from './responseStyles.js';
3
3
  import { buildProviderContext } from './contextBuilder.js';
4
+ import { buildCacheDiagnostic, latestCacheDiagnostic } from './cacheDiagnostics.js';
4
5
  import { buildDirectoryOverviewPrelude } from './directoryOverview.js';
5
6
  import { buildProjectInstructionsContext } from './projectInstructions.js';
6
7
  import { readPromptFile } from './promptFiles.js';
8
+ import { stableJson } from './stableHash.js';
7
9
  import { executeConfiguredHooks, hasHookForEvent, } from '@remi/hooks';
8
10
  import { emptyMemoryRecall, recallRelevantMemories, recordMemoryUse } from '@remi/memory';
9
- import { compactSession, estimateSessionTokens, estimateTextTokens, eventsAfterLatestCompact, latestCompactSummary, maybeUpdateSessionMemory, readSessionMemory } from '@remi/compact';
11
+ import { compactSession, estimateSessionTokens, estimateTextTokens, eventsAfterLatestCompact, latestCompactSummary, maybeUpdateSessionMemory, readSessionMemory, } from '@remi/compact';
10
12
  import { formatSkillIndexContext, loadSkillContent, loadSkillIndex } from '@remi/skills';
11
13
  export { buildBaseSystemPrompt, buildCurrentTurnSystemPrompt, buildResponseLanguageSystemPrompt, buildResponseStyleSystemPrompt, defaultResponseStyleId, isResponseStyleId, normalizeResponseStyleId, resolveResponseStyle, responseStylePresets, } from './responseStyles.js';
12
14
  export { buildProviderContext, } from './contextBuilder.js';
13
15
  export { buildProjectInstructionsContext } from './projectInstructions.js';
14
16
  export { promptFileCandidates, readPromptFile } from './promptFiles.js';
17
+ export { buildCacheDiagnosticReport, formatCacheDiagnosticStatus, } from './cacheDiagnostics.js';
18
+ export { buildCacheShapeBenchmark, buildRemiCacheShapeBenchmarkFixture, buildRemiCacheShapeBenchmarkRequest, } from './cacheBenchmark.js';
15
19
  import { createModelRouter, createProviderClient, createTokenEstimatorRegistry, } from '@remi/llm';
16
- import { existsSync } from 'node:fs';
20
+ import { existsSync, readFileSync } from 'node:fs';
17
21
  import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
18
22
  import { createSessionStore, readSessionEvents, writeToolResultArtifact } from '@remi/sessions';
19
23
  import { createMcpAwareToolRegistry, createMcpRuntime } from '@remi/mcp';
@@ -162,6 +166,11 @@ const maxAutoCompactFailures = 3;
162
166
  const maxFilesystemContextEvents = 120;
163
167
  const maxFilesystemContextRoots = 8;
164
168
  const maxFilesystemContextPaths = 12;
169
+ const maxRecentContinuationEvents = 160;
170
+ const maxRecentLoadedSkills = 3;
171
+ const maxRecentToolFacts = 8;
172
+ const maxRecentFileExcerpts = 2;
173
+ const maxRecentFileExcerptChars = 2_000;
165
174
  const maxFailedToolContextTurns = 3;
166
175
  const maxFailedToolContextTextChars = 240;
167
176
  const toolResultArtifactThresholdBytes = 16 * 1024;
@@ -191,7 +200,7 @@ export function createAgentReadOnlyToolExecutor() {
191
200
  export function createAgentLocalToolExecutor() {
192
201
  return createRemiLocalToolExecutor(createAgentToolRegistry());
193
202
  }
194
- function createContextBudget(model, config, runtimeConfig) {
203
+ export function createContextBudget(model, config, runtimeConfig) {
195
204
  const contextWindowTokens = Math.max(1, model.contextWindow);
196
205
  const configuredOutputReserve = config.maxOutputReserveTokens ?? runtimeConfig?.maxOutputReserveTokens;
197
206
  const modelOutputReserve = Math.min(model.maxOutputTokens ?? defaultMaxOutputReserveTokens, defaultMaxOutputReserveTokens);
@@ -251,18 +260,17 @@ function shouldAutoCompactSession(options) {
251
260
  maxSummaryChars: tokenBudgetToChars(options.budget.tokenBudgets.compact ?? 6_000),
252
261
  };
253
262
  }
254
- function performAutoCompactSession(options) {
263
+ async function performAutoCompactSession(options) {
255
264
  try {
256
- try {
257
- maybeUpdateSessionMemory(options.cwd, options.sessionId, { force: true, maxSummaryChars: Math.min(options.maxSummaryChars, 8_000) });
258
- }
259
- catch {
260
- // Auto compact can still proceed from transcript if session memory update fails.
261
- }
262
- const result = compactSession(options.cwd, options.sessionId, {
265
+ const result = await compactCurrentSession({
266
+ cwd: options.cwd,
267
+ sessionId: options.sessionId,
263
268
  maxSummaryChars: options.maxSummaryChars,
264
269
  trigger: 'auto',
265
- strategy: 'deterministic',
270
+ strategy: options.strategy,
271
+ env: options.env,
272
+ ...(options.provider ? { provider: options.provider } : {}),
273
+ ...(options.signal ? { signal: options.signal } : {}),
266
274
  });
267
275
  autoCompactFailuresBySession.delete(options.failureKey);
268
276
  return {
@@ -271,7 +279,7 @@ function performAutoCompactSession(options) {
271
279
  estimatedTokens: result.estimatedTokens,
272
280
  sourceEventCount: result.sourceEventCount,
273
281
  trigger: 'auto',
274
- strategy: 'deterministic',
282
+ strategy: result.strategy,
275
283
  events: readSessionEvents(options.cwd, options.sessionId),
276
284
  };
277
285
  }
@@ -280,6 +288,161 @@ function performAutoCompactSession(options) {
280
288
  return { compacted: false };
281
289
  }
282
290
  }
291
+ export async function compactCurrentSession(options) {
292
+ const env = options.env ?? process.env;
293
+ const loaded = loadRemiConfig({ cwd: options.cwd });
294
+ const strategy = options.strategy ?? loaded.config.agent?.compactStrategy ?? 'deterministic';
295
+ const trigger = options.trigger ?? 'manual';
296
+ const maxSummaryChars = options.maxSummaryChars;
297
+ if (strategy === 'model') {
298
+ const summary = await tryBuildModelCompactSummary({
299
+ cwd: options.cwd,
300
+ sessionId: options.sessionId,
301
+ env,
302
+ loadedConfig: loaded.config,
303
+ ...(options.provider ? { provider: options.provider } : {}),
304
+ ...(options.modelOverrides ? { modelOverrides: options.modelOverrides } : {}),
305
+ ...(options.responseStyle ? { responseStyle: options.responseStyle } : {}),
306
+ ...(maxSummaryChars !== undefined ? { maxSummaryChars } : {}),
307
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
308
+ ...(options.signal ? { signal: options.signal } : {}),
309
+ });
310
+ if (summary && summary.trim().length > 0) {
311
+ return compactSession(options.cwd, options.sessionId, {
312
+ trigger,
313
+ strategy: 'model',
314
+ updateSessionMemory: false,
315
+ ...(maxSummaryChars !== undefined ? { maxSummaryChars } : {}),
316
+ summaryOverride: summary,
317
+ });
318
+ }
319
+ }
320
+ return compactSession(options.cwd, options.sessionId, {
321
+ trigger,
322
+ strategy: 'deterministic',
323
+ updateSessionMemory: false,
324
+ ...(maxSummaryChars !== undefined ? { maxSummaryChars } : {}),
325
+ });
326
+ }
327
+ async function tryBuildModelCompactSummary(options) {
328
+ try {
329
+ const router = createModelRouter(options.loadedConfig, options.env);
330
+ for (const [role, modelAlias] of Object.entries(options.modelOverrides ?? {})) {
331
+ if (modelAlias) {
332
+ router.switchRoleModel(role, modelAlias);
333
+ }
334
+ }
335
+ const resolved = router.resolve('compact');
336
+ if (resolved.apiKeyStatus === 'missing') {
337
+ return undefined;
338
+ }
339
+ const providerConfig = options.loadedConfig.providers?.[resolved.providerAlias];
340
+ if (!providerConfig && !options.provider) {
341
+ return undefined;
342
+ }
343
+ const provider = options.provider ?? createProviderClient(providerConfig, options.env);
344
+ const events = readSessionEvents(options.cwd, options.sessionId);
345
+ const activeEvents = eventsAfterLatestCompact(events);
346
+ if (activeEvents.length === 0) {
347
+ return undefined;
348
+ }
349
+ const sessionMemorySummary = readSessionMemory(options.cwd, options.sessionId)?.content;
350
+ const budget = createContextBudget(resolved, {}, options.loadedConfig.agent);
351
+ const sourceBudgetTokens = Math.max(2_048, Math.floor(budget.effectiveInputBudgetTokens * 0.65));
352
+ const sourceText = buildModelCompactSource({
353
+ events,
354
+ maxChars: tokenBudgetToChars(sourceBudgetTokens),
355
+ ...(sessionMemorySummary ? { sessionMemorySummary } : {}),
356
+ });
357
+ const responseStyle = resolveResponseStyle(options.responseStyle ?? options.loadedConfig.responseStyle ?? defaultResponseStyleId);
358
+ const instruction = readPromptFile('compact-summary.md');
359
+ const messages = [
360
+ { role: 'system', content: buildBaseSystemPrompt(responseStyle.id, undefined) },
361
+ { role: 'user', content: sourceText },
362
+ { role: 'user', content: instruction },
363
+ ];
364
+ let summary = '';
365
+ for await (const chunk of provider.streamChat(messages, resolved, {
366
+ ...(options.signal ? { signal: options.signal } : {}),
367
+ timeoutMs: options.timeoutMs ?? defaultTimeoutMs,
368
+ })) {
369
+ if (typeof chunk === 'string') {
370
+ summary += chunk;
371
+ }
372
+ else if (chunk?.type === 'tool_call' || chunk?.type === 'tool_call_delta' || chunk?.type === 'tool_calls_done') {
373
+ return undefined;
374
+ }
375
+ }
376
+ return sanitizeModelCompactSummary(summary, options.maxSummaryChars);
377
+ }
378
+ catch {
379
+ return undefined;
380
+ }
381
+ }
382
+ function buildModelCompactSource(options) {
383
+ const priorSummary = latestCompactSummary(options.events);
384
+ const activeEvents = eventsAfterLatestCompact(options.events);
385
+ const sections = [
386
+ '# Session Segment To Compact',
387
+ '',
388
+ ...(priorSummary ? ['## Prior Compact Summary', '', truncateText(priorSummary, 2_000), ''] : []),
389
+ ...(options.sessionMemorySummary ? ['## Session Memory', '', truncateText(options.sessionMemorySummary, 2_000), ''] : []),
390
+ '## Active Transcript Since Latest Compact',
391
+ '',
392
+ ...activeEvents.map(formatCompactSourceEvent),
393
+ ];
394
+ return truncateText(sections.join('\n'), options.maxChars);
395
+ }
396
+ function formatCompactSourceEvent(event) {
397
+ if (event.type === 'user') {
398
+ return `- user: ${compactText(event.content, 600)}`;
399
+ }
400
+ if (event.type === 'assistant') {
401
+ return `- assistant: ${compactText(event.content, 600)}`;
402
+ }
403
+ if (event.type === 'system') {
404
+ return `- system ${event.level}: ${compactText(event.content, 300)}`;
405
+ }
406
+ if (event.type === 'skill_use') {
407
+ return `- skill loaded ${event.name}: ${event.filePath}`;
408
+ }
409
+ if (event.type === 'pending_action') {
410
+ const command = event.completionCommand ?? event.command ?? 'external action';
411
+ return `- pending action from ${event.sourceToolName}: ${compactText(command, 300)}`;
412
+ }
413
+ if (event.type === 'tool_call') {
414
+ return `- tool call ${event.toolName}: ${compactText(JSON.stringify(event.input), 300)}`;
415
+ }
416
+ if (event.type === 'tool_result') {
417
+ const status = event.ok ? 'ok' : 'failed';
418
+ const artifact = event.artifact ? ` artifact=${event.artifact.path} (${event.artifact.bytes} bytes)` : '';
419
+ return `- tool result ${event.toolName} ${status}: ${compactText(event.summary, 500)}${artifact}`;
420
+ }
421
+ if (event.type === 'compact') {
422
+ return `- compact boundary ${event.trigger ?? 'manual'} ${event.strategy ?? 'deterministic'}: ${compactText(event.summary, 400)}`;
423
+ }
424
+ if (event.type === 'cache_diagnostic') {
425
+ return `- cache diagnostic: hit=${Math.round(event.usage.cacheHitRate * 100)}% miss=${event.inference.missReason} changed=${event.inference.changedLayers.join(',') || 'none'}`;
426
+ }
427
+ return '- unknown event';
428
+ }
429
+ function sanitizeModelCompactSummary(summary, maxSummaryChars) {
430
+ const trimmed = summary.trim();
431
+ if (trimmed.length === 0) {
432
+ return undefined;
433
+ }
434
+ const sanitized = trimmed
435
+ .replace(/```(?:SEARCH\/REPLACE|search\/replace|diff)[\s\S]*?```/g, '[omitted edit block]')
436
+ .replace(/<tool_call[\s\S]*?<\/tool_call>/g, '[omitted tool call]')
437
+ .trim();
438
+ return truncateText(sanitized, maxSummaryChars ?? 6_000);
439
+ }
440
+ function truncateText(text, maxChars) {
441
+ if (text.length <= maxChars) {
442
+ return text;
443
+ }
444
+ return `${text.slice(0, Math.max(0, maxChars - 24)).trimEnd()}\n... truncated ...`;
445
+ }
283
446
  function isAutoCompactEnabled(config, runtimeConfig, env) {
284
447
  if (env['REMI_DISABLE_AUTO_COMPACT'] === '1' || env['DISABLE_COMPACT'] === '1') {
285
448
  return false;
@@ -424,6 +587,7 @@ export async function* runChatTurn(input, config = {}) {
424
587
  yield { type: 'error', sessionId: sessionStore.sessionId, message };
425
588
  return;
426
589
  }
590
+ const provider = config.provider ?? createProviderClient(providerConfig, env);
427
591
  const contextBudget = createContextBudget(resolved, config, loaded.config.agent);
428
592
  const tokenEstimatorResolution = tokenEstimatorRegistry.resolve(resolved);
429
593
  let sessionEvents = config.sessionStore ? [] : readSessionEvents(cwd, sessionStore.sessionId);
@@ -445,20 +609,25 @@ export async function* runChatTurn(input, config = {}) {
445
609
  sessionId: sessionStore.sessionId,
446
610
  failureKey: autoCompactDecision.failureKey,
447
611
  maxSummaryChars: autoCompactDecision.maxSummaryChars,
612
+ strategy: config.compactStrategy ?? loaded.config.agent?.compactStrategy ?? 'deterministic',
613
+ env,
614
+ provider: config.compactProvider ?? provider,
615
+ ...(config.signal ? { signal: config.signal } : {}),
448
616
  });
449
- if (autoCompact.compacted) {
617
+ const autoCompactResult = await autoCompact;
618
+ if (autoCompactResult.compacted) {
450
619
  yield {
451
620
  type: 'compact',
452
- summary: autoCompact.summary,
453
- estimatedTokens: autoCompact.estimatedTokens,
454
- sourceEventCount: autoCompact.sourceEventCount,
621
+ summary: autoCompactResult.summary,
622
+ estimatedTokens: autoCompactResult.estimatedTokens,
623
+ sourceEventCount: autoCompactResult.sourceEventCount,
455
624
  automatic: true,
456
- trigger: autoCompact.trigger,
457
- strategy: autoCompact.strategy,
625
+ trigger: autoCompactResult.trigger,
626
+ strategy: autoCompactResult.strategy,
458
627
  };
459
- sessionEvents = autoCompact.events;
628
+ sessionEvents = autoCompactResult.events;
460
629
  }
461
- yield { type: 'compact_end', automatic: true, trigger: 'auto', ok: autoCompact.compacted };
630
+ yield { type: 'compact_end', automatic: true, trigger: 'auto', ok: autoCompactResult.compacted };
462
631
  }
463
632
  }
464
633
  const standaloneCurrentTask = isStandaloneRepositoryInitPrompt(input);
@@ -518,11 +687,13 @@ export async function* runChatTurn(input, config = {}) {
518
687
  : undefined;
519
688
  const recentFilesystemContext = toolDefinitions.length > 0 && !standaloneCurrentTask ? buildRecentFilesystemContext(sessionEventsForContext, cwd, env) : undefined;
520
689
  const recentFailedToolContext = toolDefinitions.length > 0 && !standaloneCurrentTask ? buildRecentFailedToolContext(sessionEventsForContext) : undefined;
690
+ const recentContinuationContext = toolDefinitions.length > 0 && !standaloneCurrentTask ? buildRecentContinuationContext(sessionEventsForContext, cwd, env) : undefined;
521
691
  const projectInstructionsContext = buildProjectInstructionsContext(cwd);
692
+ const recentLoadedSkills = toolDefinitions.length > 0 && !standaloneCurrentTask ? loadRecentSessionSkills(sessionEventsForContext, skillLoadOptions) : [];
522
693
  const loadedSkills = [];
523
- const loadedSkillNames = new Set();
524
- const skillContext = formatSkillIndexContext(skillIndex);
525
- const projectInstructions = mergeProjectInstructionSections(projectInstructionsContext.content, recentFilesystemContext, recentFailedToolContext, skillContext);
694
+ const loadedSkillNames = new Set(recentLoadedSkills.map(skill => normalizeSkillNameForRuntime(skill.name)));
695
+ const skillContext = formatSkillIndexContext(skillIndex, recentLoadedSkills);
696
+ const projectInstructions = mergeProjectInstructionSections(projectInstructionsContext.content, skillContext);
526
697
  const memoryRecall = config.disableMemory || !shouldOfferMemoryForInput(input)
527
698
  ? emptyMemoryRecall(input, config.maxMemoryItems ?? defaultMaxMemoryItems)
528
699
  : recallRelevantMemories(cwd, input, {
@@ -530,11 +701,13 @@ export async function* runChatTurn(input, config = {}) {
530
701
  maxBodyChars: config.maxMemoryChars ?? defaultMaxMemoryChars,
531
702
  });
532
703
  const toolUsePrompts = toolDefinitions.length > 0 ? buildToolUseSystemPrompts(cwd, env, planPromptMode(config.planMode, planFirstRequirement), responseLanguage) : undefined;
704
+ const runtimePrompt = mergeProjectInstructionSections(toolUsePrompts?.runtimePrompt, recentFilesystemContext, recentContinuationContext, recentFailedToolContext);
533
705
  const providerContext = buildProviderContext({
534
706
  systemPrompt: buildBaseSystemPrompt(responseStyle.id, responseLanguage),
535
- ...(toolUsePrompts ? { toolPrompt: toolUsePrompts.toolPrompt, runtimePrompt: toolUsePrompts.runtimePrompt } : {}),
707
+ ...(toolUsePrompts ? { toolPrompt: toolUsePrompts.toolPrompt } : {}),
708
+ ...(runtimePrompt ? { runtimePrompt } : {}),
536
709
  ...(projectInstructions ? { projectInstructions } : {}),
537
- projectInstructionsSource: projectInstructionSource(projectInstructionsContext, Boolean(recentFilesystemContext || recentFailedToolContext)),
710
+ projectInstructionsSource: projectInstructionSource(projectInstructionsContext),
538
711
  memories: memoryRecall.memories,
539
712
  ...(sessionMemorySummary ? { sessionMemorySummary } : {}),
540
713
  ...(compactSummary ? { compactSummary } : {}),
@@ -554,6 +727,7 @@ export async function* runChatTurn(input, config = {}) {
554
727
  ...(tokenEstimatorResolution.reason ? { tokenEstimatorFallbackReason: tokenEstimatorResolution.reason } : {}),
555
728
  });
556
729
  const messages = providerContext.messages;
730
+ const previousCacheDiagnostic = latestCacheDiagnostic(sessionEvents);
557
731
  if (planExecutionApproval) {
558
732
  messages.push({ role: 'user', content: buildPlanExecutionApprovalPrompt(input, planExecutionApproval, responseLanguage) });
559
733
  }
@@ -589,7 +763,6 @@ export async function* runChatTurn(input, config = {}) {
589
763
  yield detail ? { ...event, detail } : event;
590
764
  }
591
765
  }
592
- const provider = config.provider ?? createProviderClient(providerConfig, env);
593
766
  const pendingHookRuntimeEvents = [];
594
767
  let hookCommandSequence = 0;
595
768
  const requestToolPermissionWithHooks = async (request, decision) => {
@@ -753,6 +926,10 @@ export async function* runChatTurn(input, config = {}) {
753
926
  fullToolRegistry,
754
927
  toolDefinitions,
755
928
  fullToolDefinitions,
929
+ contextTrace: providerContext.trace,
930
+ tokenEstimator: tokenEstimatorResolution.estimator,
931
+ tokenEstimateProfile: tokenEstimatorResolution.profile,
932
+ ...(previousCacheDiagnostic ? { previousCacheDiagnostic } : {}),
756
933
  skillLoadOptions,
757
934
  loadedSkillNames,
758
935
  planFirstRequirement,
@@ -773,12 +950,13 @@ export async function* runChatTurn(input, config = {}) {
773
950
  }
774
951
  }
775
952
  export async function* runAgentLoop(scope) {
776
- const { input, config, cwd, responseLanguage, provider, resolved, messages, sessionStore, modelEvent, responseStyleEvent, maxTurns, maxToolCalls, toolExecutor, fullToolRegistry, fullToolDefinitions, skillLoadOptions, loadedSkillNames, planFirstRequirement, planExecutionApproval, requiredMutationTools, shouldUseSkillDecisionGuard, hasStopHooks, recalledMemoryRefs, createToolExecutionContext, runGenericHooks, runStopHooks, runStopFailureHooks, flushPendingHookRuntimeEvents, } = scope;
953
+ const { input, config, cwd, responseLanguage, provider, resolved, messages, sessionStore, modelEvent, responseStyleEvent, maxTurns, maxToolCalls, toolExecutor, fullToolRegistry, fullToolDefinitions, contextTrace, tokenEstimator, tokenEstimateProfile, skillLoadOptions, loadedSkillNames, planFirstRequirement, planExecutionApproval, requiredMutationTools, shouldUseSkillDecisionGuard, hasStopHooks, recalledMemoryRefs, createToolExecutionContext, runGenericHooks, runStopHooks, runStopFailureHooks, flushPendingHookRuntimeEvents, } = scope;
777
954
  let activeToolRegistry = scope.activeToolRegistry;
778
955
  let toolDefinitions = scope.toolDefinitions;
779
956
  let assistantText = '';
780
957
  let usage;
781
958
  let publishedUsage;
959
+ let previousCacheDiagnostic = scope.previousCacheDiagnostic;
782
960
  let modelTurns = 0;
783
961
  let toolCallsUsed = 0;
784
962
  let mutationGuardRetries = 0;
@@ -790,6 +968,7 @@ export async function* runAgentLoop(scope) {
790
968
  let postActionApprovalGuardRetries = 0;
791
969
  let toolArgumentRetries = 0;
792
970
  let skillDecisionGuardPrompted = false;
971
+ let activeDraftPublished = false;
793
972
  const planFirstState = createPlanFirstGuardState(planFirstRequirement);
794
973
  const mutationState = createMutationGuardState(requiredMutationTools, input);
795
974
  const verificationState = createVerificationGuardState();
@@ -802,6 +981,7 @@ export async function* runAgentLoop(scope) {
802
981
  modelTurns += 1;
803
982
  let turnText = '';
804
983
  let turnReasoningText = '';
984
+ let requestUsage;
805
985
  const requestedToolCalls = [];
806
986
  const bufferTurnText = shouldBufferMutationText(mutationState) ||
807
987
  shouldGuardMutationClaims(mutationState) ||
@@ -810,6 +990,24 @@ export async function* runAgentLoop(scope) {
810
990
  shouldBufferSkillDecisionGuard(shouldUseSkillDecisionGuard, loadedSkillNames, skillDecisionGuardPrompted) ||
811
991
  hasCurrentTurnActionEvidence(mutationState, verificationState) ||
812
992
  hasStopHooks;
993
+ const previewBufferedDeltas = Boolean(config.previewBufferedDeltas && bufferTurnText);
994
+ let turnDraftPublished = false;
995
+ const discardTurnDraft = function* () {
996
+ if (!turnDraftPublished) {
997
+ return;
998
+ }
999
+ turnDraftPublished = false;
1000
+ activeDraftPublished = false;
1001
+ yield { type: 'draft_reset' };
1002
+ };
1003
+ const commitTurnDraft = function* () {
1004
+ if (!turnDraftPublished) {
1005
+ return;
1006
+ }
1007
+ turnDraftPublished = false;
1008
+ activeDraftPublished = false;
1009
+ yield { type: 'draft_commit' };
1010
+ };
813
1011
  for await (const chunk of provider.streamChat(messages, resolved, {
814
1012
  ...(config.signal ? { signal: config.signal } : {}),
815
1013
  timeoutMs: config.timeoutMs ?? defaultTimeoutMs,
@@ -817,6 +1015,7 @@ export async function* runAgentLoop(scope) {
817
1015
  })) {
818
1016
  if (typeof chunk !== 'string') {
819
1017
  if (chunk?.type === 'usage') {
1018
+ requestUsage = mergeTokenUsage(requestUsage, chunk.usage);
820
1019
  const nextUsage = mergeTokenUsage(usage, chunk.usage);
821
1020
  const usageDelta = tokenUsageDelta(publishedUsage, nextUsage);
822
1021
  usage = nextUsage;
@@ -840,27 +1039,55 @@ export async function* runAgentLoop(scope) {
840
1039
  if (!bufferTurnText) {
841
1040
  yield { type: 'delta', text: chunk };
842
1041
  }
1042
+ else if (previewBufferedDeltas) {
1043
+ turnDraftPublished = true;
1044
+ activeDraftPublished = true;
1045
+ yield { type: 'draft_delta', text: chunk };
1046
+ }
1047
+ }
1048
+ if (requestUsage) {
1049
+ const cacheDiagnostic = buildCacheDiagnostic({
1050
+ messages,
1051
+ toolDefinitions,
1052
+ contextTrace,
1053
+ model: resolved,
1054
+ turn: modelTurns,
1055
+ usage: requestUsage,
1056
+ ...(previousCacheDiagnostic ? { previous: previousCacheDiagnostic } : {}),
1057
+ tokenEstimator,
1058
+ tokenEstimateProfile,
1059
+ });
1060
+ sessionStore.append(cacheDiagnostic);
1061
+ previousCacheDiagnostic = {
1062
+ ...cacheDiagnostic,
1063
+ sessionId: sessionStore.sessionId,
1064
+ timestamp: new Date().toISOString(),
1065
+ };
843
1066
  }
844
1067
  if (requestedToolCalls.length === 0) {
845
1068
  if (shouldRetryMissingPlanFirst(planFirstState, planFirstGuardRetries, modelTurns, maxTurns)) {
846
1069
  planFirstGuardRetries += 1;
1070
+ yield* discardTurnDraft();
847
1071
  messages.push({ role: 'user', content: buildPlanFirstGuardPrompt(input, planFirstState, responseLanguage) });
848
1072
  continue;
849
1073
  }
850
1074
  if (shouldFailMissingPlanFirst(planFirstState)) {
851
1075
  const message = planFirstGuardFailureMessage(planFirstState);
852
1076
  sessionStore.append({ type: 'system', level: 'error', content: message });
1077
+ yield* discardTurnDraft();
853
1078
  yield { type: 'error', sessionId: sessionStore.sessionId, message };
854
1079
  return;
855
1080
  }
856
1081
  if (shouldRetryMissingMutationTool(mutationState, mutationGuardRetries, modelTurns, maxTurns)) {
857
1082
  mutationGuardRetries += 1;
1083
+ yield* discardTurnDraft();
858
1084
  messages.push({ role: 'user', content: buildMutationGuardPrompt(input, mutationState, responseLanguage) });
859
1085
  continue;
860
1086
  }
861
1087
  if (shouldFailMissingMutationTool(mutationState)) {
862
1088
  const message = mutationGuardFailureMessage(mutationState);
863
1089
  sessionStore.append({ type: 'system', level: 'error', content: message });
1090
+ yield* discardTurnDraft();
864
1091
  yield { type: 'error', sessionId: sessionStore.sessionId, message };
865
1092
  return;
866
1093
  }
@@ -868,11 +1095,13 @@ export async function* runAgentLoop(scope) {
868
1095
  if (mutationClaimIssue) {
869
1096
  if (mutationClaimGuardRetries < defaultMutationGuardRetries && modelTurns < maxTurns) {
870
1097
  mutationClaimGuardRetries += 1;
1098
+ yield* discardTurnDraft();
871
1099
  messages.push({ role: 'user', content: buildMutationClaimGuardPrompt(input, turnText, mutationState, mutationClaimIssue, responseLanguage) });
872
1100
  continue;
873
1101
  }
874
1102
  const message = mutationClaimGuardFailureMessage(mutationClaimIssue);
875
1103
  sessionStore.append({ type: 'system', level: 'error', content: message });
1104
+ yield* discardTurnDraft();
876
1105
  yield { type: 'error', sessionId: sessionStore.sessionId, message };
877
1106
  return;
878
1107
  }
@@ -880,11 +1109,13 @@ export async function* runAgentLoop(scope) {
880
1109
  if (verificationClaimIssue) {
881
1110
  if (verificationClaimGuardRetries < defaultVerificationClaimGuardRetries && modelTurns < maxTurns) {
882
1111
  verificationClaimGuardRetries += 1;
1112
+ yield* discardTurnDraft();
883
1113
  messages.push({ role: 'user', content: buildVerificationClaimGuardPrompt(input, turnText, verificationState, responseLanguage) });
884
1114
  continue;
885
1115
  }
886
1116
  const message = verificationClaimGuardFailureMessage(verificationState);
887
1117
  sessionStore.append({ type: 'system', level: 'error', content: message });
1118
+ yield* discardTurnDraft();
888
1119
  yield { type: 'error', sessionId: sessionStore.sessionId, message };
889
1120
  return;
890
1121
  }
@@ -892,21 +1123,25 @@ export async function* runAgentLoop(scope) {
892
1123
  if (planFirstClaimIssue) {
893
1124
  if (planFirstClaimGuardRetries < defaultPlanFirstGuardRetries && modelTurns < maxTurns) {
894
1125
  planFirstClaimGuardRetries += 1;
1126
+ yield* discardTurnDraft();
895
1127
  messages.push({ role: 'user', content: buildPlanFirstClaimGuardPrompt(input, turnText, planFirstState, planFirstClaimIssue, responseLanguage) });
896
1128
  continue;
897
1129
  }
898
1130
  const message = planFirstClaimGuardFailureMessage(planFirstClaimIssue);
899
1131
  sessionStore.append({ type: 'system', level: 'error', content: message });
1132
+ yield* discardTurnDraft();
900
1133
  yield { type: 'error', sessionId: sessionStore.sessionId, message };
901
1134
  return;
902
1135
  }
903
1136
  if (shouldRetryPlanExecutionApproval(planExecutionApproval, turnText, planExecutionApprovalRetries, modelTurns, maxTurns)) {
904
1137
  planExecutionApprovalRetries += 1;
1138
+ yield* discardTurnDraft();
905
1139
  messages.push({ role: 'user', content: buildPlanExecutionApprovalPrompt(input, planExecutionApproval, responseLanguage, true) });
906
1140
  continue;
907
1141
  }
908
1142
  if (shouldRetryPostActionApprovalDraft(turnText, mutationState, verificationState, postActionApprovalGuardRetries, modelTurns, maxTurns)) {
909
1143
  postActionApprovalGuardRetries += 1;
1144
+ yield* discardTurnDraft();
910
1145
  messages.push({ role: 'user', content: buildPostActionApprovalGuardPrompt(input, turnText, mutationState, verificationState, responseLanguage) });
911
1146
  continue;
912
1147
  }
@@ -918,19 +1153,26 @@ export async function* runAgentLoop(scope) {
918
1153
  if (stopHookResult.blockingErrors.length > 0) {
919
1154
  if (stopHookBlockCount < defaultStopHookBlockRetries && modelTurns < maxTurns) {
920
1155
  stopHookBlockCount += 1;
1156
+ yield* discardTurnDraft();
921
1157
  messages.push({ role: 'assistant', content: turnText });
922
1158
  messages.push({ role: 'user', content: buildStopHookContinuationPrompt(stopHookResult, responseLanguage) });
923
1159
  continue;
924
1160
  }
925
1161
  const message = stopHookFailureMessage(stopHookResult);
926
1162
  sessionStore.append({ type: 'system', level: 'error', content: message });
1163
+ yield* discardTurnDraft();
927
1164
  yield { type: 'error', sessionId: sessionStore.sessionId, message };
928
1165
  return;
929
1166
  }
930
1167
  }
931
1168
  assistantText += turnText;
932
1169
  if (bufferTurnText && turnText.length > 0) {
933
- yield { type: 'delta', text: turnText };
1170
+ if (turnDraftPublished) {
1171
+ yield* commitTurnDraft();
1172
+ }
1173
+ else {
1174
+ yield { type: 'delta', text: turnText };
1175
+ }
934
1176
  }
935
1177
  sessionStore.append({
936
1178
  type: 'assistant',
@@ -952,9 +1194,11 @@ export async function* runAgentLoop(scope) {
952
1194
  }
953
1195
  if (shouldRetrySkillDecisionGuard(shouldUseSkillDecisionGuard, loadedSkillNames, skillDecisionGuardPrompted, requestedToolCalls, modelTurns, maxTurns)) {
954
1196
  skillDecisionGuardPrompted = true;
1197
+ yield* discardTurnDraft();
955
1198
  messages.push({ role: 'user', content: buildSkillDecisionGuardPrompt(input, requestedToolCalls, responseLanguage) });
956
1199
  continue;
957
1200
  }
1201
+ yield* commitTurnDraft();
958
1202
  messages.push({
959
1203
  role: 'assistant',
960
1204
  content: turnText,
@@ -1011,6 +1255,10 @@ export async function* runAgentLoop(scope) {
1011
1255
  const transcriptArtifacts = hiddenPlanFirstToolCall
1012
1256
  ? new Map()
1013
1257
  : appendToolTranscriptEvents(cwd, sessionStore, outcome.transcriptEvents);
1258
+ const pendingAction = hiddenPlanFirstToolCall ? undefined : pendingActionFromToolResult(call, outcome.result);
1259
+ if (pendingAction) {
1260
+ sessionStore.append(pendingAction);
1261
+ }
1014
1262
  if (!hiddenPlanFirstToolCall) {
1015
1263
  recordMutationToolOutcome(mutationState, outcome.toolName, outcome.result, call);
1016
1264
  recordVerificationToolOutcome(verificationState, outcome.toolName, outcome.result);
@@ -1086,6 +1334,13 @@ export async function* runAgentLoop(scope) {
1086
1334
  const skillKey = normalizeSkillNameForRuntime(outcome.skill.name);
1087
1335
  if (!loadedSkillNames.has(skillKey)) {
1088
1336
  loadedSkillNames.add(skillKey);
1337
+ sessionStore.append({
1338
+ type: 'skill_use',
1339
+ name: outcome.skill.name,
1340
+ displayName: outcome.skill.displayName,
1341
+ source: outcome.skill.source,
1342
+ filePath: outcome.skill.filePath,
1343
+ });
1089
1344
  yield {
1090
1345
  type: 'skill_use',
1091
1346
  name: outcome.skill.name,
@@ -1219,6 +1474,10 @@ export async function* runAgentLoop(scope) {
1219
1474
  }
1220
1475
  catch (error) {
1221
1476
  const message = safeErrorMessage(error);
1477
+ if (activeDraftPublished) {
1478
+ activeDraftPublished = false;
1479
+ yield { type: 'draft_reset' };
1480
+ }
1222
1481
  if (isLlmRequestTimeoutMessage(message) && hasSuccessfulToolResult) {
1223
1482
  const fallbackText = llmTimeoutAfterSuccessfulToolMessage(responseLanguage);
1224
1483
  assistantText = assistantText ? `${assistantText}\n\n${fallbackText}` : fallbackText;
@@ -1549,8 +1808,8 @@ function mergeProjectInstructionSections(...sectionsToMerge) {
1549
1808
  const sections = sectionsToMerge.filter((section) => Boolean(section && section.trim().length > 0));
1550
1809
  return sections.length > 0 ? sections.join('\n\n') : undefined;
1551
1810
  }
1552
- function projectInstructionSource(projectInstructions, hasRecentContext) {
1553
- return hasRecentContext ? `${projectInstructions.source}; recent context facts` : projectInstructions.source;
1811
+ function projectInstructionSource(projectInstructions) {
1812
+ return projectInstructions.source;
1554
1813
  }
1555
1814
  function buildRecentFilesystemContext(events, cwd, env) {
1556
1815
  const recentEvents = events.slice(-maxFilesystemContextEvents);
@@ -1608,6 +1867,162 @@ function buildRecentFilesystemContext(events, cwd, env) {
1608
1867
  'For commands that must run inside one of these projects, set the tool cwd to the mapped absolute project root.',
1609
1868
  ].join('\n');
1610
1869
  }
1870
+ function loadRecentSessionSkills(events, options) {
1871
+ const loadedSkills = [];
1872
+ const seen = new Set();
1873
+ for (let index = events.length - 1; index >= 0 && loadedSkills.length < maxRecentLoadedSkills; index -= 1) {
1874
+ const event = events[index];
1875
+ if (!event || event.type !== 'skill_use') {
1876
+ continue;
1877
+ }
1878
+ const key = normalizeSkillNameForRuntime(event.name);
1879
+ if (seen.has(key)) {
1880
+ continue;
1881
+ }
1882
+ seen.add(key);
1883
+ const skill = loadSkillContent({
1884
+ ...options,
1885
+ name: event.name,
1886
+ maxContentChars: maxLoadedSkillContentChars,
1887
+ });
1888
+ if (skill) {
1889
+ loadedSkills.unshift(skill);
1890
+ }
1891
+ }
1892
+ return loadedSkills;
1893
+ }
1894
+ function buildRecentContinuationContext(events, cwd, env) {
1895
+ const recentEvents = events.slice(-maxRecentContinuationEvents);
1896
+ const toolCalls = new Map();
1897
+ const commandFacts = [];
1898
+ const fileExcerpts = [];
1899
+ const pendingActions = [];
1900
+ const home = typeof env.HOME === 'string' && env.HOME.trim().length > 0 ? normalize(resolve(env.HOME)) : undefined;
1901
+ for (const event of recentEvents) {
1902
+ if (event.type === 'tool_call') {
1903
+ toolCalls.set(event.callId, event);
1904
+ continue;
1905
+ }
1906
+ if (event.type === 'tool_result' && event.ok) {
1907
+ const call = toolCalls.get(event.callId);
1908
+ if (!call) {
1909
+ continue;
1910
+ }
1911
+ const commandFact = formatRecentCommandFact(call, event);
1912
+ if (commandFact) {
1913
+ commandFacts.push(commandFact);
1914
+ }
1915
+ const fileExcerpt = formatRecentFileExcerpt(call, event, cwd, home);
1916
+ if (fileExcerpt) {
1917
+ fileExcerpts.push(fileExcerpt);
1918
+ }
1919
+ continue;
1920
+ }
1921
+ }
1922
+ for (const event of recentEvents) {
1923
+ if (event.type !== 'pending_action') {
1924
+ continue;
1925
+ }
1926
+ const pendingAction = formatPendingActionFact(event);
1927
+ if (pendingAction) {
1928
+ pendingActions.push(pendingAction);
1929
+ }
1930
+ }
1931
+ const recentCommandFacts = uniqueMostRecent(commandFacts, maxRecentToolFacts);
1932
+ const recentFileExcerpts = uniqueMostRecent(fileExcerpts, maxRecentFileExcerpts);
1933
+ const recentPendingActions = uniqueMostRecent(pendingActions, 2);
1934
+ if (recentCommandFacts.length === 0 && recentFileExcerpts.length === 0 && recentPendingActions.length === 0) {
1935
+ return undefined;
1936
+ }
1937
+ return [
1938
+ '## Recent Tool Continuation Facts',
1939
+ 'These records come from recent Remi tool activity in this session. They may be stale if the external state changed after the recorded tool result.',
1940
+ ...(recentPendingActions.length > 0
1941
+ ? [
1942
+ '',
1943
+ 'Pending external actions:',
1944
+ ...recentPendingActions,
1945
+ ]
1946
+ : []),
1947
+ ...(recentCommandFacts.length > 0 ? ['', 'Recent successful commands:', ...recentCommandFacts] : []),
1948
+ ...(recentFileExcerpts.length > 0 ? ['', 'Recent file excerpts:', ...recentFileExcerpts] : []),
1949
+ ].join('\n');
1950
+ }
1951
+ function formatRecentCommandFact(call, result) {
1952
+ if (call.toolName !== 'run_command' && call.toolName !== 'execute_shell') {
1953
+ return undefined;
1954
+ }
1955
+ const command = typeof call.input['command'] === 'string' ? call.input['command'].trim() : '';
1956
+ if (!command) {
1957
+ return undefined;
1958
+ }
1959
+ return `- ${call.toolName}: ${compactText(command, 220)} => ${compactText(result.summary, 220)}`;
1960
+ }
1961
+ function formatRecentFileExcerpt(call, result, cwd, home) {
1962
+ if (call.toolName !== 'read_file' || !result.artifact) {
1963
+ return undefined;
1964
+ }
1965
+ const output = readToolResultArtifactContent(result.artifact);
1966
+ if (!output) {
1967
+ return undefined;
1968
+ }
1969
+ const parsed = parseToolOutput(output);
1970
+ if (!parsed || typeof parsed['content'] !== 'string') {
1971
+ return undefined;
1972
+ }
1973
+ const rawPath = typeof parsed['path'] === 'string' ? parsed['path'] : typeof call.input['path'] === 'string' ? call.input['path'] : '';
1974
+ if (!rawPath) {
1975
+ return undefined;
1976
+ }
1977
+ const normalizedPath = normalizeFilesystemCandidate(rawPath, home) ?? normalize(resolve(cwd, rawPath));
1978
+ if (isSensitiveFilesystemCandidate(normalizedPath)) {
1979
+ return undefined;
1980
+ }
1981
+ const content = truncateText(parsed['content'], maxRecentFileExcerptChars);
1982
+ return [`- ${normalizedPath}`, ' ```', indentContinuationContextBlock(content), ' ```'].join('\n');
1983
+ }
1984
+ function readToolResultArtifactContent(artifact) {
1985
+ try {
1986
+ const parsed = JSON.parse(readFileSync(artifact.path, 'utf8'));
1987
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1988
+ return undefined;
1989
+ }
1990
+ const record = parsed;
1991
+ return typeof record['content'] === 'string' ? record['content'] : undefined;
1992
+ }
1993
+ catch {
1994
+ return undefined;
1995
+ }
1996
+ }
1997
+ function indentContinuationContextBlock(text) {
1998
+ return text
1999
+ .replace(/\r\n/g, '\n')
2000
+ .split('\n')
2001
+ .map(line => ` ${line}`)
2002
+ .join('\n');
2003
+ }
2004
+ function formatPendingActionFact(event) {
2005
+ const lines = [`- ${event.sourceToolName} call ${event.callId}`];
2006
+ if (event.command) {
2007
+ lines.push(` Original command: ${compactText(event.command, 260)}`);
2008
+ }
2009
+ if (event.completionCommand) {
2010
+ lines.push(` Completion command: ${compactText(event.completionCommand, 360)}`);
2011
+ }
2012
+ else if (event.tokenName && event.token) {
2013
+ lines.push(` ${event.tokenName}: ${event.token}`);
2014
+ }
2015
+ if (event.url) {
2016
+ lines.push(` URL: ${event.url}`);
2017
+ }
2018
+ if (event.qrImagePath) {
2019
+ lines.push(` QR image: ${event.qrImagePath}`);
2020
+ }
2021
+ if (event.message) {
2022
+ lines.push(` Note: ${compactText(event.message, 220)}`);
2023
+ }
2024
+ return lines.length > 1 ? lines.join('\n') : undefined;
2025
+ }
1611
2026
  function buildRecentFailedToolContext(events) {
1612
2027
  const failedTurns = [];
1613
2028
  let currentTurn;
@@ -2693,7 +3108,15 @@ function toolDefinitionsForProvider(registry, extraTools = []) {
2693
3108
  description: tool.description,
2694
3109
  parameters: tool.inputSchema,
2695
3110
  })),
2696
- ];
3111
+ ]
3112
+ .map(tool => ({
3113
+ ...tool,
3114
+ parameters: stableJson(tool.parameters),
3115
+ }))
3116
+ .sort(compareToolDefinitions);
3117
+ }
3118
+ function compareToolDefinitions(left, right) {
3119
+ return left.name.localeCompare(right.name) || left.description.localeCompare(right.description);
2697
3120
  }
2698
3121
  function coreToolDefinitionsForProvider(index, config) {
2699
3122
  const definitions = [];
@@ -3442,6 +3865,180 @@ function summarizeToolDetail(result) {
3442
3865
  }
3443
3866
  return undefined;
3444
3867
  }
3868
+ function pendingActionFromToolResult(call, result) {
3869
+ if (!result.ok || (result.toolName !== 'run_command' && result.toolName !== 'execute_shell')) {
3870
+ return undefined;
3871
+ }
3872
+ const parsed = parseToolOutput(result.output);
3873
+ if (!parsed || typeof parsed['command'] !== 'string') {
3874
+ return undefined;
3875
+ }
3876
+ const command = parsed['command'].trim();
3877
+ const stdout = typeof parsed['stdout'] === 'string' ? parsed['stdout'] : '';
3878
+ const stderr = typeof parsed['stderr'] === 'string' ? parsed['stderr'] : '';
3879
+ const combinedOutput = joinCommandOutput(stdout, stderr);
3880
+ const structuredRecords = structuredObjectsFromText(combinedOutput);
3881
+ const token = firstStringFromRecords(structuredRecords, pendingActionTokenKeys()) ?? tokenFromText(combinedOutput);
3882
+ const tokenName = token ? tokenNameFromRecords(structuredRecords, pendingActionTokenKeys()) ?? tokenNameFromText(combinedOutput) ?? 'token' : undefined;
3883
+ const url = firstStringFromRecords(structuredRecords, pendingActionUrlKeys()) ?? urlFromText(combinedOutput);
3884
+ const qrImagePath = firstStringFromRecords(structuredRecords, ['qr_image_path', 'qrImagePath']) ?? qrImagePathFromRecords(structuredRecords);
3885
+ const message = firstStringFromRecords(structuredRecords, ['message', 'title', 'hint']);
3886
+ const completionCommand = completionCommandFromText(combinedOutput) ?? deriveCompletionCommand(command, token);
3887
+ if (!completionCommand && !token && !pendingActionSignal(command, combinedOutput, url)) {
3888
+ return undefined;
3889
+ }
3890
+ const action = {
3891
+ type: 'pending_action',
3892
+ callId: call.id,
3893
+ sourceToolName: result.toolName,
3894
+ command,
3895
+ ...(completionCommand ? { completionCommand } : {}),
3896
+ ...(tokenName ? { tokenName } : {}),
3897
+ ...(token ? { token } : {}),
3898
+ ...(url ? { url } : {}),
3899
+ ...(qrImagePath ? { qrImagePath } : {}),
3900
+ ...(message ? { message: compactText(message, 300) } : {}),
3901
+ };
3902
+ return action;
3903
+ }
3904
+ function pendingActionTokenKeys() {
3905
+ return ['complete_token', 'resume_token', 'continuation_token', 'poll_token', 'device_code', 'user_code'];
3906
+ }
3907
+ function pendingActionUrlKeys() {
3908
+ return ['verification_uri_complete', 'verification_url', 'authorization_url', 'authorize_url', 'login_url', 'url', 'open_url'];
3909
+ }
3910
+ function structuredObjectsFromText(text) {
3911
+ const records = [];
3912
+ for (const line of text.split(/\r?\n/)) {
3913
+ const trimmed = line.trim();
3914
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
3915
+ continue;
3916
+ }
3917
+ try {
3918
+ const parsed = JSON.parse(trimmed);
3919
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
3920
+ records.push(parsed);
3921
+ }
3922
+ }
3923
+ catch {
3924
+ // Non-JSON output is handled by regex fallback extractors below.
3925
+ }
3926
+ }
3927
+ return records;
3928
+ }
3929
+ function firstStringFromRecords(records, keys) {
3930
+ for (const record of records) {
3931
+ const value = findNestedString(record, keys);
3932
+ if (value) {
3933
+ return value;
3934
+ }
3935
+ }
3936
+ return undefined;
3937
+ }
3938
+ function tokenNameFromRecords(records, keys) {
3939
+ for (const record of records) {
3940
+ const key = findNestedStringKey(record, keys);
3941
+ if (key) {
3942
+ return key;
3943
+ }
3944
+ }
3945
+ return undefined;
3946
+ }
3947
+ function findNestedString(value, keys, depth = 0) {
3948
+ if (!value || typeof value !== 'object' || depth > 4) {
3949
+ return undefined;
3950
+ }
3951
+ const record = value;
3952
+ for (const key of keys) {
3953
+ const candidate = record[key];
3954
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
3955
+ return candidate.trim();
3956
+ }
3957
+ }
3958
+ for (const nested of Object.values(record)) {
3959
+ const candidate = findNestedString(nested, keys, depth + 1);
3960
+ if (candidate) {
3961
+ return candidate;
3962
+ }
3963
+ }
3964
+ return undefined;
3965
+ }
3966
+ function findNestedStringKey(value, keys, depth = 0) {
3967
+ if (!value || typeof value !== 'object' || depth > 4) {
3968
+ return undefined;
3969
+ }
3970
+ const record = value;
3971
+ for (const key of keys) {
3972
+ const candidate = record[key];
3973
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
3974
+ return key;
3975
+ }
3976
+ }
3977
+ for (const nested of Object.values(record)) {
3978
+ const candidate = findNestedStringKey(nested, keys, depth + 1);
3979
+ if (candidate) {
3980
+ return candidate;
3981
+ }
3982
+ }
3983
+ return undefined;
3984
+ }
3985
+ function tokenFromText(text) {
3986
+ const match = text.match(/\b(?:complete[_-]?token|resume[_-]?token|continuation[_-]?token|poll[_-]?token|device[_-]?code|user[_-]?code)\b["':=\s]+([A-Za-z0-9._:-]{6,})/i);
3987
+ return match?.[1];
3988
+ }
3989
+ function tokenNameFromText(text) {
3990
+ const match = text.match(/\b(complete[_-]?token|resume[_-]?token|continuation[_-]?token|poll[_-]?token|device[_-]?code|user[_-]?code)\b/i);
3991
+ return match?.[1]?.replace(/-/g, '_');
3992
+ }
3993
+ function urlFromText(text) {
3994
+ const match = text.match(/https?:\/\/[^\s"'<>]+/i);
3995
+ return match ? trimUrlTrailer(match[0]) : undefined;
3996
+ }
3997
+ function trimUrlTrailer(url) {
3998
+ return url.replace(/[),.;]+$/g, '');
3999
+ }
4000
+ function qrImagePathFromRecords(records) {
4001
+ for (const record of records) {
4002
+ const path = findNestedString(record, ['path']);
4003
+ if (path && /(?:qr|auth|login).*\.(?:png|jpg|jpeg)$/i.test(path)) {
4004
+ return path;
4005
+ }
4006
+ }
4007
+ return undefined;
4008
+ }
4009
+ function completionCommandFromText(text) {
4010
+ const match = text.match(/([^\r\n`]*\b(?:auth|oauth|sso)?\s*login\b[^\r\n`]*--complete\s+\S+[^\r\n`]*)/i);
4011
+ const command = match?.[1]?.trim();
4012
+ return command && command.length < 1_000 ? command : undefined;
4013
+ }
4014
+ function deriveCompletionCommand(command, token) {
4015
+ if (!token) {
4016
+ return undefined;
4017
+ }
4018
+ const trimmed = command.trim();
4019
+ if (!trimmed) {
4020
+ return undefined;
4021
+ }
4022
+ if (/\s--complete(?:\s|$)/.test(trimmed)) {
4023
+ return trimmed;
4024
+ }
4025
+ const safeToken = shellWordForContext(token);
4026
+ if (/\s--begin(?:\s|$)/.test(trimmed)) {
4027
+ return trimmed.replace(/\s--begin(?=\s|$)/, ` --complete ${safeToken}`);
4028
+ }
4029
+ if (/\b(?:auth|oauth|sso)?\s*login\b/i.test(trimmed)) {
4030
+ return `${trimmed} --complete ${safeToken}`;
4031
+ }
4032
+ return undefined;
4033
+ }
4034
+ function shellWordForContext(value) {
4035
+ return /^[A-Za-z0-9._:/=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
4036
+ }
4037
+ function pendingActionSignal(command, output, url) {
4038
+ return Boolean(url &&
4039
+ (/\b(?:auth|oauth|sso|login|mfa|device|verify|verification)\b/i.test(command) ||
4040
+ /\b(?:action_required|device_code|login_status|pending|verification_uri|authenticate|confirm|scan)\b/i.test(output)));
4041
+ }
3445
4042
  function isRecoverableToolProtocolError(result) {
3446
4043
  if (result.ok) {
3447
4044
  return false;
@@ -3534,7 +4131,7 @@ function pathIsUrlRecoveryGuidance(result) {
3534
4131
  return [
3535
4132
  'Do not call filesystem tools for URLs.',
3536
4133
  'Use web_fetch for public http(s) URLs.',
3537
- 'For authenticated internal ByteDance, Feishu, or Lark URLs, load a relevant internal-platform skill such as bytedcli when it is available.',
4134
+ 'For authenticated internal document URLs, load a relevant internal-platform skill when one is available.',
3538
4135
  ].join(' ');
3539
4136
  }
3540
4137
  function runCommandRecoveryGuidance(result, call) {