@sema-agent/core 2.12.0 → 3.0.0

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 (115) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +1 -0
  2. package/dist/agents/agent-transcript-tool.js +1 -1
  3. package/dist/agents/cascade.d.ts +4 -0
  4. package/dist/agents/cascade.js +54 -9
  5. package/dist/agents/repair-loop.d.ts +2 -0
  6. package/dist/agents/repair-loop.js +101 -2
  7. package/dist/agents/retain-ledger.d.ts +18 -2
  8. package/dist/agents/retain-ledger.js +37 -8
  9. package/dist/agents/roster-store.d.ts +4 -0
  10. package/dist/agents/roster-store.js +22 -1
  11. package/dist/agents/teacher.js +4 -2
  12. package/dist/agents/team.js +7 -4
  13. package/dist/agents/verify.d.ts +15 -2
  14. package/dist/agents/verify.js +36 -1
  15. package/dist/bin/sema-tb.js +2 -2
  16. package/dist/brain/circuit-breaker.d.ts +1 -0
  17. package/dist/brain/circuit-breaker.js +26 -9
  18. package/dist/brain/degrading.js +13 -1
  19. package/dist/brain/failover.js +2 -0
  20. package/dist/brain/repetition.js +10 -1
  21. package/dist/brain/routing.js +5 -1
  22. package/dist/brain/status-sink.js +5 -1
  23. package/dist/brain/stream-engine.js +4 -1
  24. package/dist/brain/tool-call-repair.js +32 -14
  25. package/dist/core/auto-compaction.js +17 -7
  26. package/dist/core/auto-promote.js +2 -1
  27. package/dist/core/checkpoint-store.d.ts +1 -0
  28. package/dist/core/checkpoint-store.js +28 -10
  29. package/dist/core/consolidate-scope.js +4 -3
  30. package/dist/core/context-edit.js +3 -0
  31. package/dist/core/context-guard.js +3 -0
  32. package/dist/core/git-worktree-env.d.ts +6 -1
  33. package/dist/core/git-worktree-env.js +17 -1
  34. package/dist/core/hooks.d.ts +1 -0
  35. package/dist/core/hooks.js +40 -2
  36. package/dist/core/lsp-diagnostics.js +13 -7
  37. package/dist/core/lsp-protocol.d.ts +1 -1
  38. package/dist/core/lsp-protocol.js +20 -8
  39. package/dist/core/lsp-session.d.ts +12 -2
  40. package/dist/core/lsp-session.js +114 -49
  41. package/dist/core/lsp.d.ts +13 -1
  42. package/dist/core/lsp.js +64 -9
  43. package/dist/core/mcp.d.ts +1 -0
  44. package/dist/core/mcp.js +4 -3
  45. package/dist/core/memory-engine/data-plane.js +4 -0
  46. package/dist/core/memory-engine/engine.d.ts +2 -1
  47. package/dist/core/memory-engine/engine.js +82 -7
  48. package/dist/core/memory-engine/file-backend.d.ts +1 -0
  49. package/dist/core/memory-engine/file-backend.js +13 -3
  50. package/dist/core/memory-engine/layout.js +12 -1
  51. package/dist/core/memory.js +3 -0
  52. package/dist/core/permission-rules.d.ts +2 -1
  53. package/dist/core/permission-rules.js +24 -3
  54. package/dist/core/runner/active-skill-scope.js +34 -6
  55. package/dist/core/runner/assemble-result.d.ts +1 -1
  56. package/dist/core/runner/assemble-result.js +0 -1
  57. package/dist/core/runner/prepare-task.js +23 -3
  58. package/dist/core/runner/runtask.js +48 -30
  59. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  60. package/dist/core/runner/tool-disclosure.js +26 -7
  61. package/dist/core/runner/usage-accounting.d.ts +6 -0
  62. package/dist/core/runner/usage-accounting.js +9 -8
  63. package/dist/core/secret-env.js +5 -1
  64. package/dist/core/sensitive-path-policy.js +9 -3
  65. package/dist/core/session-store.js +15 -4
  66. package/dist/core/skill-tool-specifier.d.ts +8 -0
  67. package/dist/core/skill-tool-specifier.js +58 -0
  68. package/dist/core/skills-directory.d.ts +1 -1
  69. package/dist/core/skills-directory.js +16 -4
  70. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  71. package/dist/core/task-registry-monitor.js +7 -1
  72. package/dist/core/tool-policy.js +65 -17
  73. package/dist/core/trace.d.ts +1 -0
  74. package/dist/core/types.d.ts +3 -5
  75. package/dist/core/with-retry.js +0 -1
  76. package/dist/engine/compaction/compaction.d.ts +1 -0
  77. package/dist/engine/compaction/compaction.js +14 -5
  78. package/dist/engine/lsp/frame-decoder.js +6 -3
  79. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  80. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  81. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  82. package/dist/index.d.ts +1 -1
  83. package/dist/internal/harness.d.ts +1 -1
  84. package/dist/internal/harness.js +1 -1
  85. package/dist/internal/llm.d.ts +1 -1
  86. package/dist/orchestration/workflow-governance.js +43 -9
  87. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  88. package/dist/orchestration/workflow-script-store.js +34 -4
  89. package/dist/orchestration/workflow.js +10 -6
  90. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  91. package/dist/prompt-assembly/artifact-store.js +39 -24
  92. package/dist/prompt-assembly/assemble.js +38 -7
  93. package/dist/prompt-assembly/epoch.js +19 -2
  94. package/dist/prompt-assembly/event-registry.js +3 -3
  95. package/dist/prompt-assembly/explain.js +1 -1
  96. package/dist/prompt-assembly/tool-catalog.js +16 -1
  97. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  98. package/dist/stores/file/fs-atomic.d.ts +3 -0
  99. package/dist/stores/file/fs-atomic.js +53 -8
  100. package/dist/stores/file/index.d.ts +1 -0
  101. package/dist/stores/file/index.js +7 -0
  102. package/dist/stores/file/session-store.d.ts +2 -0
  103. package/dist/stores/file/session-store.js +28 -1
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  105. package/dist/tools/fs/bash-readonly-classifier.js +57 -8
  106. package/dist/tools/fs/fs-bash.d.ts +1 -1
  107. package/dist/tools/fs/fs-bash.js +93 -5
  108. package/dist/tools/loop-tick.js +1 -1
  109. package/dist/tools/monitor.js +55 -9
  110. package/dist/tools/scheduler-tools.js +9 -3
  111. package/dist/tools/web.d.ts +8 -2
  112. package/dist/tools/web.js +46 -17
  113. package/dist/tools/worktree.d.ts +2 -0
  114. package/dist/tools/worktree.js +233 -143
  115. package/package.json +5 -2
@@ -222,6 +222,7 @@ function resumeContinuation(resume) {
222
222
  }
223
223
  function makeTurnBoundary(prepared, stats, rs, deps) {
224
224
  const { spec, queue, manualCompactRef, todoToolMounted, taskToolsMounted, turnToolSpan, walltimeDeadlineMs, walltimeMonotonicDeadline, nudgeSchedule, buildFinalizeText, timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact, runnerHooks } = deps;
225
+ let finalVerifySeenAtLastBoundary = 0;
225
226
  const onTurnBoundary = async (event) => {
226
227
  if (prepared.callCapRef && turnToolSpan.startMin !== undefined && turnToolSpan.endMax !== undefined) {
227
228
  recordToolCycleSample(prepared.callCapRef.state, turnToolSpan.endMax - turnToolSpan.startMin);
@@ -340,8 +341,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
340
341
  const scan = await Promise.race([
341
342
  prepared.detectExternalChanges(maxFiles),
342
343
  new Promise((resolve) => {
343
- const t = setTimeout(() => resolve({ changed: [], evicted: [] }), SCAN_CEILING_MS);
344
- t.unref?.();
344
+ setTimeout(() => resolve({ changed: [], evicted: [] }), SCAN_CEILING_MS);
345
345
  }),
346
346
  ]);
347
347
  for (const p of scan.evicted)
@@ -474,8 +474,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
474
474
  probed = await Promise.race([
475
475
  rs.attach.instrProbe(prepared.instructionSources),
476
476
  new Promise((resolve) => {
477
- const t = setTimeout(() => resolve(null), PROBE_CEILING_MS);
478
- t.unref?.();
477
+ setTimeout(() => resolve(null), PROBE_CEILING_MS);
479
478
  }),
480
479
  ]);
481
480
  }
@@ -499,11 +498,13 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
499
498
  }
500
499
  }
501
500
  let batchContextBlock;
501
+ const finalVerifyInjectedThisTurn = rs.counters.finalVerifyInjections !== finalVerifySeenAtLastBoundary;
502
+ finalVerifySeenAtLastBoundary = rs.counters.finalVerifyInjections;
502
503
  if (postToolBatchHook !== undefined && rs.turn.toolBatch.length > 0) {
503
504
  const batch = rs.turn.toolBatch;
504
505
  rs.turn.toolBatch = [];
505
506
  batchArgs?.clear();
506
- if (!boundarySteered && !rs.counters.finalizeInjected && rs.counters.finalVerifyInjections === 0 && !prepared.abortController.signal.aborted) {
507
+ if (!boundarySteered && !rs.counters.finalizeInjected && !finalVerifyInjectedThisTurn && !prepared.abortController.signal.aborted) {
507
508
  try {
508
509
  const r = await postToolBatchHook(batch);
509
510
  if (r?.additionalContext) {
@@ -808,16 +809,18 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
808
809
  rs.turn.turnUsageMissing = true;
809
810
  const u = m.usage;
810
811
  stats.tokens += u.totalTokens || 0;
811
- const { totalInputTokens: turnInput, costMicroUsd: turnCostMicroUsd } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
812
+ const { totalInputTokens: turnTotal, uncachedInputTokens: turnUncached, costMicroUsd: turnCostMicroUsd, } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
812
813
  if (!rs.telemetry.pricingConfigured)
813
814
  rs.telemetry.unpricedSpend = true;
814
- stats.promptTokens += turnInput;
815
+ stats.promptTokens += turnUncached;
816
+ stats.totalInputTokens += turnTotal;
815
817
  stats.cachedTokens += u.cacheRead || 0;
816
818
  stats.cacheWriteTokens += u.cacheWrite || 0;
817
819
  stats.outputTokens += u.output || 0;
818
820
  stats.costMicroUsd += turnCostMicroUsd;
819
- rs.turn.turnUsage ??= { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
820
- rs.turn.turnUsage.inputTokens += turnInput;
821
+ rs.turn.turnUsage ??= { inputTokens: 0, totalInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
822
+ rs.turn.turnUsage.inputTokens += turnUncached;
823
+ rs.turn.turnUsage.totalInputTokens += turnTotal;
821
824
  rs.turn.turnUsage.outputTokens += u.output || 0;
822
825
  rs.turn.turnUsage.cacheReadTokens += u.cacheRead || 0;
823
826
  rs.turn.turnUsage.cacheWriteTokens += u.cacheWrite || 0;
@@ -843,7 +846,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
843
846
  model: m.model,
844
847
  provider: m.provider,
845
848
  turn: stats.turns + 1,
846
- promptTokens: turnInput,
849
+ promptTokens: turnUncached,
850
+ totalInputTokens: turnTotal,
847
851
  completionTokens: u.output || 0,
848
852
  cacheRead: u.cacheRead || 0,
849
853
  cacheWrite: u.cacheWrite || 0,
@@ -1259,7 +1263,6 @@ export class Runner {
1259
1263
  let t;
1260
1264
  const timeout = new Promise((res2) => {
1261
1265
  t = setTimeout(() => res2(undefined), READY_TIMEOUT_MS);
1262
- t.unref?.();
1263
1266
  });
1264
1267
  return Promise.race([p, timeout]).finally(() => clearTimeout(t));
1265
1268
  };
@@ -1647,7 +1650,7 @@ export class Runner {
1647
1650
  }
1648
1651
  }
1649
1652
  onReady({ harness: prepared.harness, abortController: prepared.abortController });
1650
- const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
1653
+ const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
1651
1654
  prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
1652
1655
  if (resume &&
1653
1656
  resume.cp.suspendedAt !== undefined &&
@@ -1662,6 +1665,9 @@ export class Runner {
1662
1665
  const gateToolName = "toolName" in resume.cp.gate ? resume.cp.gate.toolName : undefined;
1663
1666
  prepared.humanReviewRef.gates.push({ kind: resume.cp.gate.kind, waitMs, ...(decision !== undefined ? { decision } : {}), ...(gateToolName !== undefined ? { toolName: gateToolName } : {}) });
1664
1667
  }
1668
+ if (resume !== undefined && resume.outcome.gate === "plan_review" && resume.outcome.decision === "reject") {
1669
+ prepared.planModeRef.active = true;
1670
+ }
1665
1671
  const rs = createRunState();
1666
1672
  rs.telemetry.cacheFamily = cacheFamilyOf(prepared.model);
1667
1673
  rs.telemetry.pricing = this.deps.pricing?.[prepared.model.id] ?? modelCostToPricing(prepared.model.cost);
@@ -2126,9 +2132,10 @@ export class Runner {
2126
2132
  const priced = this.deps.pricing?.[m.id] !== undefined || m.cost !== undefined;
2127
2133
  if (!priced)
2128
2134
  rs.telemetry.unpricedSpend = true;
2129
- const { totalInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
2135
+ const { totalInputTokens, uncachedInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
2130
2136
  stats.tokens += u.totalTokens || 0;
2131
- stats.promptTokens += totalInputTokens;
2137
+ stats.promptTokens += uncachedInputTokens;
2138
+ stats.totalInputTokens += totalInputTokens;
2132
2139
  stats.cachedTokens += u.cacheRead || 0;
2133
2140
  stats.cacheWriteTokens += u.cacheWrite || 0;
2134
2141
  stats.outputTokens += u.output || 0;
@@ -2136,7 +2143,7 @@ export class Runner {
2136
2143
  stats.compactionMicroUsd = (stats.compactionMicroUsd ?? 0) + costMicroUsd;
2137
2144
  emitTrace(rs.telemetry.tracer, () => ({
2138
2145
  kind: "brain.call", version: 1, taskId: rs.telemetry.taskId, model: m.id, provider: m.provider,
2139
- promptTokens: totalInputTokens, completionTokens: u.output || 0,
2146
+ promptTokens: uncachedInputTokens, totalInputTokens, completionTokens: u.output || 0,
2140
2147
  cacheRead: u.cacheRead || 0, cacheWrite: u.cacheWrite || 0,
2141
2148
  latencyMs: 0, ...(priced ? { costMicroUsd } : {}), ...(typeof msg?.stopReason === "string" ? { stopReason: msg.stopReason } : {}), ts: Date.now(),
2142
2149
  }));
@@ -2531,13 +2538,26 @@ export class Runner {
2531
2538
  let promptBlocked = false;
2532
2539
  const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
2533
2540
  if (userPromptSubmit) {
2534
- const decision = await userPromptSubmit(spec.objective);
2535
- if (decision?.block) {
2536
- prepared.blockedRef.reason = formatHookFeedback(decision.block);
2537
- promptBlocked = true;
2541
+ try {
2542
+ const decision = await userPromptSubmit(spec.objective);
2543
+ if (decision?.block) {
2544
+ prepared.blockedRef.reason = formatHookFeedback(decision.block);
2545
+ promptBlocked = true;
2546
+ }
2547
+ else if (decision?.additionalContext) {
2548
+ effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${spec.objective}`;
2549
+ }
2538
2550
  }
2539
- else if (decision?.additionalContext) {
2540
- effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${spec.objective}`;
2551
+ catch (hookErr) {
2552
+ const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
2553
+ try {
2554
+ this.deps.onError?.(err, { phase: "hook", sessionId: prepared.sessionId });
2555
+ }
2556
+ catch {
2557
+ }
2558
+ prepared.blockedRef.reason = formatHookFeedback(`the deployment's userPromptSubmit hook crashed while screening this prompt (${err.message}); ` +
2559
+ `the prompt was NOT submitted (fail-closed)`);
2560
+ promptBlocked = true;
2541
2561
  }
2542
2562
  }
2543
2563
  const firstFrames = [];
@@ -2722,17 +2742,17 @@ export class Runner {
2722
2742
  costMicroUsd: prepared.nestedStats.costMicroUsd || undefined,
2723
2743
  };
2724
2744
  }
2725
- if (stats.promptTokens > 0) {
2726
- const rawHit = stats.cachedTokens / stats.promptTokens;
2727
- if (stats.cachedTokens > stats.promptTokens) {
2728
- this.deps.onError?.(new Error(`prompt-cache: usage/api mismatch — cachedTokens (${stats.cachedTokens}) > promptTokens (${stats.promptTokens}); the cache family is likely wrong. Does model.api match the brain serving it? Override with model.params.promptCacheFamily = "input-includes-cached" | "input-excludes-cached".`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2745
+ if (stats.totalInputTokens > 0) {
2746
+ const rawHit = stats.cachedTokens / stats.totalInputTokens;
2747
+ if (stats.cachedTokens > stats.totalInputTokens) {
2748
+ this.deps.onError?.(new Error(`prompt-cache: usage/api mismatch — cachedTokens (${stats.cachedTokens}) > totalInputTokens (${stats.totalInputTokens}); the cache family is likely wrong. Does model.api match the brain serving it? Override with model.params.promptCacheFamily = "input-includes-cached" | "input-excludes-cached".`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2729
2749
  }
2730
2750
  stats.cacheHitRate = Math.min(1, rawHit);
2731
- if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.promptTokens >= 8000 && stats.cacheHitRate < 0.15) {
2751
+ if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.totalInputTokens >= 8000 && stats.cacheHitRate < 0.15) {
2732
2752
  const cause = rs.degrade.degraded
2733
2753
  ? `a mid-task model switch (${rs.degrade.degraded.from} → ${rs.degrade.degraded.to}, degradation) reset the prefix cache — this is the likely cause`
2734
2754
  : `the cacheable prefix looks unstable. Keep volatile content (memory/timestamps/ids) out of the prompt prefix and tool order stable, or it's a server-side/TTL miss`;
2735
- this.deps.onError?.(new Error(`prompt-cache: low prefix-cache hit rate ${(stats.cacheHitRate * 100).toFixed(0)}% over ${stats.turns} turns (${stats.promptTokens} prompt tokens) — ${cause}. See design/09.`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2755
+ this.deps.onError?.(new Error(`prompt-cache: low prefix-cache hit rate ${(stats.cacheHitRate * 100).toFixed(0)}% over ${stats.turns} turns (${stats.totalInputTokens} prompt tokens) — ${cause}. See design/09.`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2736
2756
  }
2737
2757
  }
2738
2758
  if (prepared.resourceLedger &&
@@ -3016,7 +3036,6 @@ export class Runner {
3016
3036
  ac.abort();
3017
3037
  resolve([]);
3018
3038
  }, SUGGESTIONS_TIMEOUT_MS);
3019
- timer.unref?.();
3020
3039
  });
3021
3040
  const work = (async () => {
3022
3041
  let model = prepared.model;
@@ -3088,7 +3107,6 @@ export class Runner {
3088
3107
  ac.abort();
3089
3108
  resolve({ ok: false, error: { code: "aborted", message: "snapshot timed out" } });
3090
3109
  }, SNAPSHOT_TIMEOUT_MS);
3091
- timer.unref?.();
3092
3110
  });
3093
3111
  const work = store.snapshot(prepared.sessionId, leafId, env, root, ac.signal);
3094
3112
  work.catch(() => { });
@@ -3102,7 +3120,7 @@ export class Runner {
3102
3120
  if (!r.ok) {
3103
3121
  const tooLarge = r.error.code === "too_large";
3104
3122
  const refusedAt = Date.now();
3105
- if (tooLarge)
3123
+ if (tooLarge && !this.snapshotTooLargeRoots.has(root))
3106
3124
  this.snapshotTooLargeRoots.set(root, { refusedAt, skipAnnounced: false });
3107
3125
  try {
3108
3126
  this.deps.onError?.(new Error(`rewind-files snapshot failed (${r.error.code}): ${r.error.message}` +
@@ -57,4 +57,5 @@ export declare function createToolSearchTool(opts: {
57
57
  listingRide?: (newlyActivated: readonly string[]) => string | undefined;
58
58
  mountedNames?: () => ReadonlySet<string>;
59
59
  directCallEnabled?: boolean;
60
+ serializeActivation?: <T>(section: () => Promise<T>) => Promise<T>;
60
61
  }): AgentTool;
@@ -172,11 +172,22 @@ export function resolveToolSearch(args, registry) {
172
172
  export function extractDiscoveredToolNames(messages, registry) {
173
173
  const names = new Set();
174
174
  const pendingDirect = new Map();
175
+ const pendingSearch = new Map();
175
176
  for (const m of messages) {
176
177
  if (m.role === "toolResult") {
177
178
  const name = pendingDirect.get(m.toolCallId);
178
- if (name !== undefined && m.isError !== true)
179
- names.add(name);
179
+ if (name !== undefined) {
180
+ pendingDirect.delete(m.toolCallId);
181
+ if (m.isError !== true)
182
+ names.add(name);
183
+ }
184
+ const searched = pendingSearch.get(m.toolCallId);
185
+ if (searched !== undefined) {
186
+ pendingSearch.delete(m.toolCallId);
187
+ if (m.isError !== true)
188
+ for (const n of searched)
189
+ names.add(n);
190
+ }
180
191
  continue;
181
192
  }
182
193
  if (m.role !== "assistant")
@@ -185,15 +196,18 @@ export function extractDiscoveredToolNames(messages, registry) {
185
196
  if (part.type !== "toolCall")
186
197
  continue;
187
198
  if (part.name === TOOL_SEARCH_NAME) {
188
- for (const n of resolveToolSearch(part.arguments, registry)) {
189
- names.add(n);
190
- }
199
+ const prior = pendingSearch.get(part.id);
200
+ const resolved = resolveToolSearch(part.arguments, registry);
201
+ pendingSearch.set(part.id, prior === undefined ? resolved : [...prior, ...resolved]);
191
202
  }
192
203
  else if (registry.has(part.name)) {
193
204
  pendingDirect.set(part.id, part.name);
194
205
  }
195
206
  }
196
207
  }
208
+ for (const list of pendingSearch.values())
209
+ for (const n of list)
210
+ names.add(n);
197
211
  return [...names];
198
212
  }
199
213
  export function createToolSearchTool(opts) {
@@ -209,6 +223,12 @@ export function createToolSearchTool(opts) {
209
223
  "first. When any instruction, reminder, or another tool's description names a deferred tool, activate it " +
210
224
  'with query "select:<name>" before calling it. ';
211
225
  let activationChain = Promise.resolve();
226
+ const serializeActivation = opts.serializeActivation ??
227
+ ((section) => {
228
+ const p = activationChain.then(section);
229
+ activationChain = p.then(() => undefined, () => undefined);
230
+ return p;
231
+ });
212
232
  return defineTool({
213
233
  name: TOOL_SEARCH_NAME,
214
234
  contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
@@ -259,7 +279,7 @@ export function createToolSearchTool(opts) {
259
279
  },
260
280
  };
261
281
  }
262
- const section = activationChain.then(async () => {
282
+ const section = serializeActivation(async () => {
263
283
  const newly = matched.filter((n) => !active.has(n));
264
284
  if (newly.length > 0) {
265
285
  for (const n of newly)
@@ -275,7 +295,6 @@ export function createToolSearchTool(opts) {
275
295
  }
276
296
  return { newly, ride: newly.length > 0 ? listingRide?.(newly) : undefined };
277
297
  });
278
- activationChain = section.then(() => undefined, () => undefined);
279
298
  const { newly, ride } = await section;
280
299
  const lines = matched.map((n) => {
281
300
  const info = registry.get(n);
@@ -7,6 +7,11 @@ export declare function promptTokensOf(family: CacheFamily, u: {
7
7
  cacheRead: number;
8
8
  cacheWrite: number;
9
9
  }): number;
10
+ export declare function uncachedInputTokensOf(family: CacheFamily, u: {
11
+ input: number;
12
+ cacheRead: number;
13
+ cacheWrite: number;
14
+ }): number;
10
15
  export declare function usageCostMicroUsd(family: CacheFamily, u: {
11
16
  input?: number;
12
17
  output?: number;
@@ -14,5 +19,6 @@ export declare function usageCostMicroUsd(family: CacheFamily, u: {
14
19
  cacheWrite?: number;
15
20
  }, pricing: ModelPricing): {
16
21
  totalInputTokens: number;
22
+ uncachedInputTokens: number;
17
23
  costMicroUsd: number;
18
24
  };
@@ -12,18 +12,19 @@ export function cacheFamilyOf(model) {
12
12
  export function promptTokensOf(family, u) {
13
13
  return family === "input-excludes-cached" ? u.input + u.cacheRead + u.cacheWrite : u.input;
14
14
  }
15
+ export function uncachedInputTokensOf(family, u) {
16
+ return Math.max(0, promptTokensOf(family, u) - u.cacheRead - u.cacheWrite);
17
+ }
15
18
  export function usageCostMicroUsd(family, u, pricing) {
16
- const totalInputTokens = promptTokensOf(family, {
17
- input: u.input || 0,
18
- cacheRead: u.cacheRead || 0,
19
- cacheWrite: u.cacheWrite || 0,
20
- });
19
+ const normalized = { input: u.input || 0, cacheRead: u.cacheRead || 0, cacheWrite: u.cacheWrite || 0 };
20
+ const totalInputTokens = promptTokensOf(family, normalized);
21
+ const uncachedInputTokens = uncachedInputTokensOf(family, normalized);
21
22
  const costMicroUsd = computeCostMicroUsd({
22
23
  totalInputTokens,
23
- cacheReadTokens: u.cacheRead || 0,
24
- cacheWriteTokens: u.cacheWrite || 0,
24
+ cacheReadTokens: normalized.cacheRead,
25
+ cacheWriteTokens: normalized.cacheWrite,
25
26
  cacheWriteTokensLong: 0,
26
27
  outputTokens: u.output || 0,
27
28
  }, pricing);
28
- return { totalInputTokens, costMicroUsd };
29
+ return { totalInputTokens, uncachedInputTokens, costMicroUsd };
29
30
  }
@@ -1,5 +1,9 @@
1
1
  const SECRET_ENV_RE = /(^|_)(KEY|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY|ACCESS_KEY|SESSION_TOKEN)(?:_(?:\d+|ID))?$/i;
2
- const SECRET_ENV_EXACT_NAMES = new Set(["DATABASE_URL", "CONNECTION_STRING", "DSN", "NPM_AUTH"]);
2
+ const SECRET_ENV_EXACT_NAMES = new Set([
3
+ "DATABASE_URL", "CONNECTION_STRING", "DSN", "NPM_AUTH",
4
+ "PGPASSWORD", "MYSQL_PWD", "SSHPASS", "GPG_PASSPHRASE", "GH_PAT", "GITHUB_PAT",
5
+ "NPM_CONFIG__AUTH", "NPM_CONFIG__AUTHTOKEN",
6
+ ]);
3
7
  export function isSecretEnvKey(key) {
4
8
  return SECRET_ENV_RE.test(key) || SECRET_ENV_EXACT_NAMES.has(key.toUpperCase());
5
9
  }
@@ -14,6 +14,9 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
14
14
  ".ssh",
15
15
  ".git/hooks",
16
16
  ".git/config",
17
+ ".git/modules",
18
+ ".mcp.json",
19
+ ".claude*",
17
20
  ".aws",
18
21
  ".config/gcloud",
19
22
  ".azure",
@@ -24,15 +27,18 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
24
27
  ".zsh_history",
25
28
  ];
26
29
  function segmentGlobToRegExp(segment) {
27
- const escaped = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
30
+ const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*/g, "[^/]*");
28
31
  return new RegExp(`^${escaped}$`);
29
32
  }
30
33
  function compilePatterns(patterns) {
31
34
  const out = [];
32
35
  for (const raw of patterns) {
33
36
  const segments = raw.split("/").filter(Boolean).map(segmentGlobToRegExp);
34
- if (segments.length > 0)
35
- out.push({ raw, segments });
37
+ if (segments.length === 0) {
38
+ throw new Error(`createSensitivePathPolicy: pattern ${JSON.stringify(raw)} contains no path segments and would guard nothing. ` +
39
+ `Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".git/hooks", "*.pem"); remove the entry or spell the segments.`);
40
+ }
41
+ out.push({ raw, segments });
36
42
  }
37
43
  return out;
38
44
  }
@@ -114,6 +114,11 @@ export class TtlSessionStore {
114
114
  }
115
115
  const forked = await this.repo.fork(await source.getMetadata(), forkParams);
116
116
  const meta = await forked.getMetadata();
117
+ const inherited = this.owners.has(sourceId) ? this.owners.get(sourceId) : undefined;
118
+ const forkOwner = owner ?? inherited;
119
+ if (forkOwner !== undefined && !this.owners.has(meta.id)) {
120
+ this.owners.set(meta.id, forkOwner);
121
+ }
117
122
  this.entries.set(meta.id, {
118
123
  session: forked,
119
124
  lastActiveAt: Date.now(),
@@ -149,8 +154,13 @@ export class TtlSessionStore {
149
154
  return this.owners.has(sessionId) ? this.owners.get(sessionId) : undefined;
150
155
  }
151
156
  async register(sessionId, owner) {
152
- if (!this.owners.has(sessionId))
153
- this.owners.set(sessionId, owner);
157
+ if (this.owners.has(sessionId)) {
158
+ return;
159
+ }
160
+ if (owner !== null && typeof owner !== "string") {
161
+ throw new TypeError(`SessionStore.register: owner must be a string (owned) or null (anonymous), got ${owner === undefined ? "undefined" : typeof owner}`);
162
+ }
163
+ this.owners.set(sessionId, owner);
154
164
  }
155
165
  sweep(now = Date.now()) {
156
166
  for (const [id, e] of this.entries) {
@@ -160,8 +170,9 @@ export class TtlSessionStore {
160
170
  if (now - e.lastActiveAt > this.defaultTtlMs) {
161
171
  this.entries.delete(id);
162
172
  if (this.evictPolicy === "delete") {
163
- void this.repo.delete({ id, createdAt: "" });
164
- this.owners.delete(id);
173
+ void this.repo.delete({ id, createdAt: "" }).then(() => {
174
+ this.owners.delete(id);
175
+ }, () => { });
165
176
  }
166
177
  }
167
178
  }
@@ -0,0 +1,8 @@
1
+ export interface SkillToolEntry {
2
+ readonly raw: string;
3
+ readonly name: string;
4
+ readonly specifier?: string;
5
+ }
6
+ export declare function parseSkillToolEntry(entry: string): SkillToolEntry;
7
+ export declare function isSkillSpecifierEnforced(canonicalName: string): boolean;
8
+ export declare function skillSpecifierRejection(entry: SkillToolEntry, args: unknown): string | undefined;
@@ -0,0 +1,58 @@
1
+ import { parsePermissionRule, wildcardMatch } from "./permission-rules.js";
2
+ import { canonicalToolName } from "./tool-name-aliases.js";
3
+ import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
4
+ import { parseLeadingCommandName } from "../tools/fs/bash-readonly-classifier.js";
5
+ const SPECIFIER_ENFORCED_TOOLS = new Set(COARSE_SHELL_TOOLS.map(canonicalToolName));
6
+ export function parseSkillToolEntry(entry) {
7
+ const parsed = parsePermissionRule(entry);
8
+ const name = canonicalToolName(parsed.toolName);
9
+ return parsed.ruleContent === undefined ? { raw: entry, name } : { raw: entry, name, specifier: parsed.ruleContent };
10
+ }
11
+ export function isSkillSpecifierEnforced(canonicalName) {
12
+ return SPECIFIER_ENFORCED_TOOLS.has(canonicalName);
13
+ }
14
+ function normalizeSpacing(s) {
15
+ return s.trim().replace(/[ \t]+/g, " ");
16
+ }
17
+ function hasUnescapedStar(content) {
18
+ for (let i = 0; i < content.length; i++) {
19
+ if (content[i] !== "*")
20
+ continue;
21
+ let backslashes = 0;
22
+ for (let j = i - 1; j >= 0 && content[j] === "\\"; j--)
23
+ backslashes++;
24
+ if (backslashes % 2 === 0)
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+ function specifierMatchesCommand(specifier, command) {
30
+ const spec = normalizeSpacing(specifier);
31
+ const prefix = /^(.+):\*$/.exec(spec)?.[1];
32
+ if (prefix !== undefined) {
33
+ return command === prefix || command.startsWith(prefix + " ");
34
+ }
35
+ if (hasUnescapedStar(spec))
36
+ return wildcardMatch(spec, command);
37
+ return command === spec;
38
+ }
39
+ export function skillSpecifierRejection(entry, args) {
40
+ const { specifier } = entry;
41
+ if (specifier === undefined)
42
+ return undefined;
43
+ if (!isSkillSpecifierEnforced(entry.name)) {
44
+ return `entry "${entry.raw}" narrows a tool whose calls this gate cannot match against a command pattern — only the shell tools (${[...SPECIFIER_ENFORCED_TOOLS].join(", ")}) carry a command string, so the entry admits nothing`;
45
+ }
46
+ const command = args?.command;
47
+ if (typeof command !== "string") {
48
+ return `entry "${entry.raw}" requires a command string to match against, and this call has none`;
49
+ }
50
+ const parsedCommand = parseLeadingCommandName(command);
51
+ if ("reject" in parsedCommand) {
52
+ return `entry "${entry.raw}" only admits a single simple command (${parsedCommand.reject})`;
53
+ }
54
+ if (!specifierMatchesCommand(specifier, normalizeSpacing(command))) {
55
+ return `command "${command.trim()}" is not admitted by entry "${entry.raw}"`;
56
+ }
57
+ return undefined;
58
+ }
@@ -1,5 +1,5 @@
1
1
  import type { SkillSpec } from "./types.js";
2
- export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
2
+ export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "allowed_tool_pattern_unsupported" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
3
3
  export interface SkillsDirectoryWarning {
4
4
  code: SkillsDirectoryWarningCode;
5
5
  skill: string;
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
2
  import { isAbsolute, join, relative } from "node:path";
3
3
  import { canonicalToolName } from "./tool-name-aliases.js";
4
+ import { isSkillSpecifierEnforced, parseSkillToolEntry } from "./skill-tool-specifier.js";
4
5
  const SKILL_FILE = "SKILL.md";
5
6
  const RESOURCE_DIRS = ["assets", "references", "scripts"];
6
7
  const NAME_MAX_CHARS = 64;
@@ -316,12 +317,23 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
316
317
  const names = [];
317
318
  const seen = new Set();
318
319
  for (const n of splitToolNames(declared)) {
319
- const key = canonicalToolName(n);
320
+ const entry = parseSkillToolEntry(n);
321
+ const key = entry.specifier === undefined ? entry.name : `${entry.name}(${entry.specifier})`;
320
322
  if (seen.has(key))
321
323
  continue;
322
324
  seen.add(key);
323
325
  names.push(n);
324
326
  }
327
+ for (const n of names) {
328
+ const entry = parseSkillToolEntry(n);
329
+ if (entry.specifier !== undefined && !isSkillSpecifierEnforced(entry.name)) {
330
+ warn({
331
+ code: "allowed_tool_pattern_unsupported",
332
+ skill: skillName,
333
+ detail: `allowed-tools entry "${n}" narrows a tool whose calls cannot be matched against a command pattern — only shell tools carry one, so the entry is not enforced and the tool is DISABLED inside this skill frame. Use the bare tool name to allow it fully.`,
334
+ });
335
+ }
336
+ }
325
337
  if (names.length === 0)
326
338
  return undefined;
327
339
  if (names.some((n) => n === ALL_TOOLS_WILDCARD))
@@ -329,9 +341,9 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
329
341
  let allowTools = names;
330
342
  if (deployedTools !== undefined) {
331
343
  const mounted = new Set(deployedTools.map(canonicalToolName));
332
- allowTools = names.filter((n) => mounted.has(canonicalToolName(n)));
344
+ allowTools = names.filter((n) => mounted.has(parseSkillToolEntry(n).name));
333
345
  for (const n of names) {
334
- if (!mounted.has(canonicalToolName(n))) {
346
+ if (!mounted.has(parseSkillToolEntry(n).name)) {
335
347
  warn({
336
348
  code: "allowed_tool_not_mounted",
337
349
  skill: skillName,
@@ -342,7 +354,7 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
342
354
  }
343
355
  if (disallowed.length > 0) {
344
356
  const denied = new Set(disallowed.map(canonicalToolName));
345
- allowTools = allowTools.filter((n) => !denied.has(canonicalToolName(n)));
357
+ allowTools = allowTools.filter((n) => !denied.has(parseSkillToolEntry(n).name));
346
358
  }
347
359
  return { allowTools, lineageId: `skill:${skillName}` };
348
360
  }
@@ -32,5 +32,45 @@ export async function sessionRepoContract(make, runAssertion) {
32
32
  const repo = make();
33
33
  await assert.rejects(repo.open({ id: "0198fa00-0000-7000-8000-0000000000ff", createdAt: "" }), /not.?found/i);
34
34
  });
35
+ run("delete() makes the id not_found — the history is gone, not merely hidden from open()", async () => {
36
+ const repo = make();
37
+ const sess = await repo.create({ id: A });
38
+ await sess.getStorage().appendEntry(SESSION_ENTRY("e1", null));
39
+ await repo.delete({ id: A, createdAt: "" });
40
+ await assert.rejects(repo.open({ id: A, createdAt: "" }), /not.?found/i);
41
+ const fresh = await repo.create({ id: A });
42
+ assert.deepEqual((await fresh.getStorage().getEntries()).map((e) => e.id), []);
43
+ });
44
+ run("fork() copies the log, stamps forkedFrom, and leaves the SOURCE untouched", async () => {
45
+ const repo = make();
46
+ const src = await repo.create({ id: A });
47
+ await src.getStorage().appendEntry(SESSION_ENTRY("e1", null));
48
+ await src.getStorage().appendEntry(SESSION_ENTRY("e2", "e1"));
49
+ const B = "0198fa00-0000-7000-8000-00000000000b";
50
+ const forked = await repo.fork({ id: A, createdAt: "" }, { id: B });
51
+ assert.deepEqual((await forked.getStorage().getEntries()).map((e) => e.id), ["e1", "e2"]);
52
+ assert.equal((await forked.getMetadata()).forkedFrom, A, "fork lineage must be stamped (a reap walks it)");
53
+ await src.getStorage().appendEntry(SESSION_ENTRY("e3", "e2"));
54
+ assert.deepEqual((await (await repo.open({ id: A, createdAt: "" })).getStorage().getEntries()).map((e) => e.id), ["e1", "e2", "e3"]);
55
+ assert.deepEqual((await (await repo.open({ id: B, createdAt: "" })).getStorage().getEntries()).map((e) => e.id), ["e1", "e2"]);
56
+ });
57
+ run("exportEntries → importEntries round-trips the log verbatim, and the imported session REPLACES what was there", async () => {
58
+ const repo = make();
59
+ if (repo.exportEntries === undefined || repo.importEntries === undefined)
60
+ return;
61
+ const src = await repo.create({ id: A });
62
+ await src.getStorage().appendEntry(SESSION_ENTRY("e1", null));
63
+ await src.getStorage().appendEntry(SESSION_ENTRY("e2", "e1"));
64
+ const donor = await repo.exportEntries(A);
65
+ assert.deepEqual(donor.map((e) => e.id), ["e1", "e2"]);
66
+ const C = "0198fa00-0000-7000-8000-00000000000c";
67
+ const target = await repo.create({ id: C });
68
+ await target.getStorage().appendEntry(SESSION_ENTRY("old", null));
69
+ await repo.importEntries(C, undefined, donor);
70
+ const after = await repo.exportEntries(C);
71
+ assert.deepEqual(after.map((e) => e.id), ["e1", "e2"]);
72
+ assert.deepEqual(after.map((e) => e.parentId), [null, "e1"]);
73
+ assert.equal((await repo.exportEntries(A)).length, 2, "importing into C must not disturb the donor session");
74
+ });
35
75
  await settle();
36
76
  }
@@ -42,7 +42,13 @@ export function registerMonitorLane(core, input) {
42
42
  ...(input.sessionId !== undefined ? { spillSessionId: input.sessionId } : {}),
43
43
  };
44
44
  core.handles.set(id, handle);
45
- startMonitorWatcherLane(core, id);
45
+ try {
46
+ startMonitorWatcherLane(core, id);
47
+ }
48
+ catch (e) {
49
+ core.handles.delete(id);
50
+ throw e;
51
+ }
46
52
  return id;
47
53
  }
48
54
  function spillRolledMonitorChunk(handle, stream, dropped) {