@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.
- package/dist/agents/agent-transcript-tool.d.ts +1 -0
- package/dist/agents/agent-transcript-tool.js +1 -1
- package/dist/agents/cascade.d.ts +4 -0
- package/dist/agents/cascade.js +54 -9
- package/dist/agents/repair-loop.d.ts +2 -0
- package/dist/agents/repair-loop.js +101 -2
- package/dist/agents/retain-ledger.d.ts +18 -2
- package/dist/agents/retain-ledger.js +37 -8
- package/dist/agents/roster-store.d.ts +4 -0
- package/dist/agents/roster-store.js +22 -1
- package/dist/agents/teacher.js +4 -2
- package/dist/agents/team.js +7 -4
- package/dist/agents/verify.d.ts +15 -2
- package/dist/agents/verify.js +36 -1
- package/dist/bin/sema-tb.js +2 -2
- package/dist/brain/circuit-breaker.d.ts +1 -0
- package/dist/brain/circuit-breaker.js +26 -9
- package/dist/brain/degrading.js +13 -1
- package/dist/brain/failover.js +2 -0
- package/dist/brain/repetition.js +10 -1
- package/dist/brain/routing.js +5 -1
- package/dist/brain/status-sink.js +5 -1
- package/dist/brain/stream-engine.js +4 -1
- package/dist/brain/tool-call-repair.js +32 -14
- package/dist/core/auto-compaction.js +17 -7
- package/dist/core/auto-promote.js +2 -1
- package/dist/core/checkpoint-store.d.ts +1 -0
- package/dist/core/checkpoint-store.js +28 -10
- package/dist/core/consolidate-scope.js +4 -3
- package/dist/core/context-edit.js +3 -0
- package/dist/core/context-guard.js +3 -0
- package/dist/core/git-worktree-env.d.ts +6 -1
- package/dist/core/git-worktree-env.js +17 -1
- package/dist/core/hooks.d.ts +1 -0
- package/dist/core/hooks.js +40 -2
- package/dist/core/lsp-diagnostics.js +13 -7
- package/dist/core/lsp-protocol.d.ts +1 -1
- package/dist/core/lsp-protocol.js +20 -8
- package/dist/core/lsp-session.d.ts +12 -2
- package/dist/core/lsp-session.js +114 -49
- package/dist/core/lsp.d.ts +13 -1
- package/dist/core/lsp.js +64 -9
- package/dist/core/mcp.d.ts +1 -0
- package/dist/core/mcp.js +4 -3
- package/dist/core/memory-engine/data-plane.js +4 -0
- package/dist/core/memory-engine/engine.d.ts +2 -1
- package/dist/core/memory-engine/engine.js +82 -7
- package/dist/core/memory-engine/file-backend.d.ts +1 -0
- package/dist/core/memory-engine/file-backend.js +13 -3
- package/dist/core/memory-engine/layout.js +12 -1
- package/dist/core/memory.js +3 -0
- package/dist/core/permission-rules.d.ts +2 -1
- package/dist/core/permission-rules.js +24 -3
- package/dist/core/runner/active-skill-scope.js +34 -6
- package/dist/core/runner/assemble-result.d.ts +1 -1
- package/dist/core/runner/assemble-result.js +0 -1
- package/dist/core/runner/prepare-task.js +23 -3
- package/dist/core/runner/runtask.js +48 -30
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +26 -7
- package/dist/core/runner/usage-accounting.d.ts +6 -0
- package/dist/core/runner/usage-accounting.js +9 -8
- package/dist/core/secret-env.js +5 -1
- package/dist/core/sensitive-path-policy.js +9 -3
- package/dist/core/session-store.js +15 -4
- package/dist/core/skill-tool-specifier.d.ts +8 -0
- package/dist/core/skill-tool-specifier.js +58 -0
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +16 -4
- package/dist/core/store-contracts/session-repo-contract.js +40 -0
- package/dist/core/task-registry-monitor.js +7 -1
- package/dist/core/tool-policy.js +65 -17
- package/dist/core/trace.d.ts +1 -0
- package/dist/core/types.d.ts +3 -5
- package/dist/core/with-retry.js +0 -1
- package/dist/engine/compaction/compaction.d.ts +1 -0
- package/dist/engine/compaction/compaction.js +14 -5
- package/dist/engine/lsp/frame-decoder.js +6 -3
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +103 -26
- package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
- package/dist/index.d.ts +1 -1
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/workflow-governance.js +43 -9
- package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
- package/dist/orchestration/workflow-script-store.js +34 -4
- package/dist/orchestration/workflow.js +10 -6
- package/dist/prompt-assembly/artifact-store.d.ts +2 -0
- package/dist/prompt-assembly/artifact-store.js +39 -24
- package/dist/prompt-assembly/assemble.js +38 -7
- package/dist/prompt-assembly/epoch.js +19 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/explain.js +1 -1
- package/dist/prompt-assembly/tool-catalog.js +16 -1
- package/dist/prompt-assembly/turn-snapshot.js +4 -1
- package/dist/stores/file/fs-atomic.d.ts +3 -0
- package/dist/stores/file/fs-atomic.js +53 -8
- package/dist/stores/file/index.d.ts +1 -0
- package/dist/stores/file/index.js +7 -0
- package/dist/stores/file/session-store.d.ts +2 -0
- package/dist/stores/file/session-store.js +28 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
- package/dist/tools/fs/bash-readonly-classifier.js +57 -8
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +93 -5
- package/dist/tools/loop-tick.js +1 -1
- package/dist/tools/monitor.js +55 -9
- package/dist/tools/scheduler-tools.js +9 -3
- package/dist/tools/web.d.ts +8 -2
- package/dist/tools/web.js +46 -17
- package/dist/tools/worktree.d.ts +2 -0
- package/dist/tools/worktree.js +233 -143
- 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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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:
|
|
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 +=
|
|
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 +=
|
|
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:
|
|
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 +=
|
|
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
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
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
|
-
|
|
2540
|
-
|
|
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.
|
|
2726
|
-
const rawHit = stats.cachedTokens / stats.
|
|
2727
|
-
if (stats.cachedTokens > stats.
|
|
2728
|
-
this.deps.onError?.(new Error(`prompt-cache: usage/api mismatch — cachedTokens (${stats.cachedTokens}) >
|
|
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.
|
|
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.
|
|
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
|
|
179
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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 =
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
24
|
-
cacheWriteTokens:
|
|
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
|
}
|
package/dist/core/secret-env.js
CHANGED
|
@@ -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([
|
|
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(/[.+^${}()|[\]
|
|
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
|
|
35
|
-
|
|
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 (
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
344
|
+
allowTools = names.filter((n) => mounted.has(parseSkillToolEntry(n).name));
|
|
333
345
|
for (const n of names) {
|
|
334
|
-
if (!mounted.has(
|
|
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(
|
|
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
|
-
|
|
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) {
|