@sema-agent/core 2.13.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/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/tool-call-repair.js +32 -14
- package/dist/core/auto-compaction.js +17 -7
- 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 +12 -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/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/assemble-result.d.ts +1 -1
- package/dist/core/runner/assemble-result.js +0 -1
- package/dist/core/runner/prepare-task.js +11 -0
- package/dist/core/runner/runtask.js +21 -22
- 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/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 +1 -0
- 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/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +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 +2 -2
- 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 +19 -2
- 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/worktree.d.ts +2 -0
- package/dist/tools/worktree.js +233 -143
- package/package.json +5 -2
|
@@ -147,6 +147,18 @@ export class MemoryEngine {
|
|
|
147
147
|
handle.baseIds.set(path, entry.id);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
const indexGate = this.gateDerivedIndex(handle);
|
|
151
|
+
if (indexGate !== undefined) {
|
|
152
|
+
try {
|
|
153
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
154
|
+
kind: "gate",
|
|
155
|
+
at: this.now(),
|
|
156
|
+
items: [`memory index rejected and rebuilt: ${inlineUntrusted(indexGate.reason)}`],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
150
162
|
const indexText = this.rebuildIndex(handle, headers, { write: writeScope !== null });
|
|
151
163
|
handle.indexBaselineLines = countIndexLines(indexText);
|
|
152
164
|
handle.indexText = indexText;
|
|
@@ -364,6 +376,7 @@ export class MemoryEngine {
|
|
|
364
376
|
basePathById.set(id, p);
|
|
365
377
|
const patches = [];
|
|
366
378
|
const pendingProjections = [];
|
|
379
|
+
const idsAddedThisHarvest = new Set();
|
|
367
380
|
let processed = 0;
|
|
368
381
|
for (let i = 0; i < kept.length; i++) {
|
|
369
382
|
const f = kept[i];
|
|
@@ -465,7 +478,28 @@ export class MemoryEngine {
|
|
|
465
478
|
patches.push({ op: "update", id: parsed.id, entry: { ...entry, id: parsed.id, rev: computeEntryRev({ id: parsed.id, frontmatter: fm, body: parsed.body }) }, ...(revByBasePath.get(renamedFrom) !== undefined ? { baseRev: revByBasePath.get(renamedFrom) } : {}) });
|
|
466
479
|
continue;
|
|
467
480
|
}
|
|
468
|
-
|
|
481
|
+
let reboundId;
|
|
482
|
+
if (parsed.id !== undefined && (renamedFrom !== undefined || idsAddedThisHarvest.has(parsed.id))) {
|
|
483
|
+
reboundId = parsed.id;
|
|
484
|
+
const idless = serializeEntryFile({ id: "", frontmatter: fm, body: parsed.body }).replace(/^id:[ \t]*$\r?\n/m, "");
|
|
485
|
+
try {
|
|
486
|
+
writeFileSync(f.canonical, idless, "utf8");
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
report.rejections.push({
|
|
490
|
+
path: rel,
|
|
491
|
+
code: "filename",
|
|
492
|
+
reason: `memory write blocked: this file carries the entry id of another memory file (a copy is not a rename) and its frontmatter could not be rewritten (${err instanceof Error ? err.message : String(err)}) — remove the "id:" line so it can be committed as a new entry`,
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
id = uuidv7();
|
|
497
|
+
entry.id = id;
|
|
498
|
+
entry.rev = computeEntryRev(entry);
|
|
499
|
+
report.warnings.push(`${rel}: frontmatter id ${reboundId} already has a projection (${renamedFrom !== undefined ? relative(handle.memoryDir, renamedFrom) : "another new file in this harvest"}) — a copy is not a rename, so this file was committed as a NEW entry with a fresh id; the existing entry was left untouched`);
|
|
500
|
+
}
|
|
501
|
+
pendingProjections.push({ path: f.canonical, entry, needed: minted || reboundId !== undefined || needsCompletion(parsed, fm) });
|
|
502
|
+
idsAddedThisHarvest.add(entry.id);
|
|
469
503
|
patches.push({ op: "add", id: entry.id, entry });
|
|
470
504
|
}
|
|
471
505
|
let patchReport;
|
|
@@ -503,12 +537,15 @@ export class MemoryEngine {
|
|
|
503
537
|
const drained = this.backend.drainInboundFindings?.();
|
|
504
538
|
if (drained !== undefined && drained.length > 0)
|
|
505
539
|
report.inboundFindings = drained;
|
|
540
|
+
const indexRejection = this.gateDerivedIndex(handle);
|
|
541
|
+
if (indexRejection !== undefined)
|
|
542
|
+
report.rejections.push(indexRejection);
|
|
506
543
|
const headers = await this.backend.listHeaders([...new Set([...handle.scopes, writeScope])]);
|
|
507
544
|
handle.indexText = this.rebuildIndex(handle, headers, { write: true }, report.warnings);
|
|
508
|
-
await this.rebaseline(handle);
|
|
545
|
+
await this.rebaseline(handle, new Set(report.degraded?.pending ?? []));
|
|
509
546
|
return report;
|
|
510
547
|
}
|
|
511
|
-
async rebaseline(handle) {
|
|
548
|
+
async rebaseline(handle, keepBaseline = new Set()) {
|
|
512
549
|
const writeScope = handle.writeScope;
|
|
513
550
|
if (writeScope === null)
|
|
514
551
|
return;
|
|
@@ -535,12 +572,17 @@ export class MemoryEngine {
|
|
|
535
572
|
if (id === undefined)
|
|
536
573
|
continue;
|
|
537
574
|
const stub = stubs.get(path);
|
|
538
|
-
const
|
|
575
|
+
const relPath = relative(handle.memoryDir, path);
|
|
576
|
+
const deferred = keepBaseline.has(relPath);
|
|
577
|
+
const priorRev = deferred ? handle.baseRevs.get(path) : undefined;
|
|
578
|
+
const priorId = deferred ? handle.baseIds.get(path) : undefined;
|
|
579
|
+
const rev = priorRev ?? revOfText(text, priorId ?? id);
|
|
580
|
+
const baselineId = priorId ?? id;
|
|
539
581
|
next.push({
|
|
540
582
|
path,
|
|
541
|
-
relPath
|
|
583
|
+
relPath,
|
|
542
584
|
scope: writeScope,
|
|
543
|
-
id,
|
|
585
|
+
id: baselineId,
|
|
544
586
|
slug: f.slug,
|
|
545
587
|
rev,
|
|
546
588
|
stub: stub !== undefined && stub.rev === rev,
|
|
@@ -548,7 +590,7 @@ export class MemoryEngine {
|
|
|
548
590
|
});
|
|
549
591
|
fileToScope.set(path, writeScope);
|
|
550
592
|
baseRevs.set(path, rev);
|
|
551
|
-
baseIds.set(path,
|
|
593
|
+
baseIds.set(path, baselineId);
|
|
552
594
|
}
|
|
553
595
|
handle.materialized = next;
|
|
554
596
|
handle.fileToScope = fileToScope;
|
|
@@ -650,6 +692,39 @@ export class MemoryEngine {
|
|
|
650
692
|
return viaBackend;
|
|
651
693
|
return readSafe(join(this.controlDir, "shadow", `${id}.md`));
|
|
652
694
|
}
|
|
695
|
+
gateDerivedIndex(handle) {
|
|
696
|
+
if (handle.writeScope === null)
|
|
697
|
+
return undefined;
|
|
698
|
+
const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
|
|
699
|
+
const text = readSafe(indexPath);
|
|
700
|
+
if (text === undefined || text === "")
|
|
701
|
+
return undefined;
|
|
702
|
+
const finding = scanMemoryWrite(text).find((f) => f.code === "secret");
|
|
703
|
+
if (finding === undefined)
|
|
704
|
+
return undefined;
|
|
705
|
+
let detail = "";
|
|
706
|
+
try {
|
|
707
|
+
const dest = join(this.controlDir, "quarantine", `${this.now()}-${MEMORY_INDEX_FILENAME}`);
|
|
708
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
709
|
+
writeFileSync(dest, text, "utf8");
|
|
710
|
+
}
|
|
711
|
+
catch (err) {
|
|
712
|
+
detail = `; quarantine capture failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
713
|
+
}
|
|
714
|
+
let cleared = false;
|
|
715
|
+
try {
|
|
716
|
+
writeFileSync(indexPath, "", "utf8");
|
|
717
|
+
cleared = true;
|
|
718
|
+
}
|
|
719
|
+
catch (err) {
|
|
720
|
+
detail = `${detail}; the derived index could NOT be cleared: ${err instanceof Error ? err.message : String(err)}`;
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
path: MEMORY_INDEX_FILENAME,
|
|
724
|
+
code: finding.code,
|
|
725
|
+
reason: `memory index blocked: ${finding.reason} (${cleared ? "index cleared and rebuilt from entry frontmatter; the offending prose was captured to quarantine" : "index still carries the offending content"})${detail}`,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
653
728
|
quarantineFile(path, content) {
|
|
654
729
|
const dest = join(this.controlDir, "quarantine", `${this.now()}-${basename(path)}`);
|
|
655
730
|
let captured;
|
|
@@ -79,10 +79,10 @@ export class FileMemoryEngineBackend {
|
|
|
79
79
|
this.controlPlaneRoot = opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), dir);
|
|
80
80
|
ensureDirExists(dir);
|
|
81
81
|
ensureDirExists(this.controlPlaneRoot);
|
|
82
|
-
this.recoverJournal();
|
|
82
|
+
this.recoverJournal({ unlocked: true });
|
|
83
83
|
}
|
|
84
84
|
checkControlPlane() {
|
|
85
|
-
this.recoverJournal();
|
|
85
|
+
this.recoverJournal({ unlocked: true });
|
|
86
86
|
this.ledger = undefined;
|
|
87
87
|
this.loadLedger();
|
|
88
88
|
registeredScopes(this.controlPlaneRoot);
|
|
@@ -164,8 +164,10 @@ export class FileMemoryEngineBackend {
|
|
|
164
164
|
return;
|
|
165
165
|
atomicWriteFileSync(join(this.controlPlaneRoot, LEDGER_FILE), `${JSON.stringify(this.ledger, null, 2)}\n`);
|
|
166
166
|
}
|
|
167
|
-
recoverJournal() {
|
|
167
|
+
recoverJournal(opts = {}) {
|
|
168
168
|
const jp = join(this.controlPlaneRoot, JOURNAL_FILE);
|
|
169
|
+
if (opts.unlocked && this.txnInFlight())
|
|
170
|
+
return;
|
|
169
171
|
const raw = readSafe(jp);
|
|
170
172
|
if (raw !== undefined) {
|
|
171
173
|
let journal;
|
|
@@ -205,6 +207,14 @@ export class FileMemoryEngineBackend {
|
|
|
205
207
|
}
|
|
206
208
|
this.sweepStagedFiles();
|
|
207
209
|
}
|
|
210
|
+
txnInFlight() {
|
|
211
|
+
try {
|
|
212
|
+
return this.now() - statSync(this.txnLockDir()).mtimeMs < TXN_LOCK_STALE_MS;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
208
218
|
sweepStagedFiles() {
|
|
209
219
|
const walk = (d, depth) => {
|
|
210
220
|
let names;
|
|
@@ -216,11 +216,22 @@ function acquireSidecarLock(lockDir, opts = {}) {
|
|
|
216
216
|
try {
|
|
217
217
|
mkdirSync(lockDir);
|
|
218
218
|
const token = `${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
219
|
+
let fd;
|
|
219
220
|
try {
|
|
220
|
-
|
|
221
|
+
fd = openSync(join(lockDir, "owner"), "w");
|
|
222
|
+
writeSync(fd, token);
|
|
221
223
|
}
|
|
222
224
|
catch {
|
|
223
225
|
}
|
|
226
|
+
finally {
|
|
227
|
+
if (fd !== undefined) {
|
|
228
|
+
try {
|
|
229
|
+
closeSync(fd);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
224
235
|
return token;
|
|
225
236
|
}
|
|
226
237
|
catch {
|
package/dist/core/memory.js
CHANGED
|
@@ -32,6 +32,9 @@ const SECRET_PATTERNS = [
|
|
|
32
32
|
{ label: "slack_token", re: /\bxox[baprs]-(?:[A-Za-z0-9]+-){1,3}[A-Za-z0-9]{16,}\b/ },
|
|
33
33
|
{ label: "google_api_key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
34
34
|
{ label: "private_key", re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/ },
|
|
35
|
+
{ label: "jwt", re: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{16,}\b/ },
|
|
36
|
+
{ label: "azure_storage_account_key", re: /AccountKey=[A-Za-z0-9+/]{40,}={0,2}/ },
|
|
37
|
+
{ label: "bearer_token", re: /\bAuthorization:\s*Bearer\s+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{20,}=*/ },
|
|
35
38
|
];
|
|
36
39
|
export function detectSecret(text) {
|
|
37
40
|
if (!text)
|
|
@@ -11,7 +11,7 @@ export interface ParsedPermissionRule {
|
|
|
11
11
|
}
|
|
12
12
|
export interface PermissionRuleIssue {
|
|
13
13
|
rule: string;
|
|
14
|
-
code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix" | "unsupported.file_glob" | "unsupported.mcp_paren";
|
|
14
|
+
code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix" | "unsupported.file_glob" | "unsupported.mcp_paren" | "unsupported.unknown_param";
|
|
15
15
|
message: string;
|
|
16
16
|
}
|
|
17
17
|
export interface PermissionRuleCaps {
|
|
@@ -30,5 +30,6 @@ export interface PermissionRulePolicyOptions {
|
|
|
30
30
|
}
|
|
31
31
|
export declare function parsePermissionRule(rule: string): ParsedPermissionRule;
|
|
32
32
|
export declare function wildcardMatch(pattern: string, value: string): boolean;
|
|
33
|
+
export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
|
|
33
34
|
export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps">): PermissionRuleIssue[];
|
|
34
35
|
export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): ToolPolicy;
|
|
@@ -90,6 +90,18 @@ const PRIMARY_FIELDS = {
|
|
|
90
90
|
WebSearch: new Set(["query"]),
|
|
91
91
|
};
|
|
92
92
|
const FILE_TOOLS = new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Grep", "Glob"]);
|
|
93
|
+
const FILE_TOOL_PARAMS = {
|
|
94
|
+
Read: new Set(["file_path", "path", "offset", "limit", "pages"]),
|
|
95
|
+
Edit: new Set(["file_path", "path", "old_string", "new_string", "replace_all"]),
|
|
96
|
+
Write: new Set(["file_path", "path", "content"]),
|
|
97
|
+
NotebookEdit: new Set(["notebook_path", "path", "cell_id", "cell_type", "edit_mode", "new_source"]),
|
|
98
|
+
Grep: new Set([
|
|
99
|
+
"pattern", "path", "glob", "type", "output_mode", "head_limit", "max_results", "offset",
|
|
100
|
+
"multiline", "context", "ignore_case", "-i", "-n", "-o", "-A", "-B", "-C",
|
|
101
|
+
]),
|
|
102
|
+
Glob: new Set(["pattern", "path", "max_results"]),
|
|
103
|
+
};
|
|
104
|
+
export const fileToolParamVocabulary = (canonicalTool) => FILE_TOOL_PARAMS[canonicalTool];
|
|
93
105
|
function countStars(s) {
|
|
94
106
|
let n = 0;
|
|
95
107
|
for (const c of s)
|
|
@@ -151,9 +163,18 @@ function compile(rules, caps, primaryFieldGeneric) {
|
|
|
151
163
|
continue;
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
|
-
else if (FILE_TOOLS.has(toolName)
|
|
155
|
-
|
|
156
|
-
|
|
166
|
+
else if (FILE_TOOLS.has(toolName)) {
|
|
167
|
+
if (colon <= 0) {
|
|
168
|
+
bad(text, "unsupported.file_glob", "file-tool glob rules (`Edit(src/**)`) are the tool-specific lane — not supported by the generic param DSL");
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const paramPart = content.slice(0, colon).trim();
|
|
172
|
+
const vocabulary = FILE_TOOL_PARAMS[toolName];
|
|
173
|
+
if (vocabulary !== undefined && !vocabulary.has(paramPart)) {
|
|
174
|
+
bad(text, "unsupported.unknown_param", `"${paramPart}" is not a parameter of ${toolName} (it carries ${[...vocabulary].sort().join(", ")}) — the rule would compile and never match. ` +
|
|
175
|
+
`For a path glob use the tool-specific lane (\`${toolName}(src/**)\` is not the generic param DSL); a Windows path such as \`${toolName}(C:/foo)\` parses as the param "C".`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
157
178
|
}
|
|
158
179
|
if (colon <= 0) {
|
|
159
180
|
bad(text, "invalid.param_split", "param-level ruleContent must be `param:pattern`");
|
|
@@ -35,7 +35,6 @@ function assistantText(msg) {
|
|
|
35
35
|
export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
36
36
|
const taskId = spec.taskId ?? sessionId;
|
|
37
37
|
const text = final ? assistantText(final) : "";
|
|
38
|
-
stats.totalInputTokens = stats.promptTokens;
|
|
39
38
|
if (!flags.unpricedSpend) {
|
|
40
39
|
const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
|
|
41
40
|
const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
|
|
@@ -433,6 +433,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
433
433
|
}
|
|
434
434
|
const taskScope = internals?.registryScope ?? spec.principal ?? "default";
|
|
435
435
|
const offloadScope = spec.principal ?? "default";
|
|
436
|
+
if (internals?.registryScope !== undefined && internals.registryScope !== offloadScope) {
|
|
437
|
+
deps.onError?.(new Error(`offload/tool-result namespace note: this run mounts in registry domain "${internals.registryScope}" but its offloaded tool results live under the TRUST identity "${offloadScope}" (principal${spec.principal === undefined ? " absent → default" : ""}). Before 2.13.0 the registry domain named this namespace too; if a deployment used registryScope as its ref-tenancy boundary, that boundary is now the principal — pass one, or read refs under "${offloadScope}".`), { phase: "config", sessionId });
|
|
438
|
+
}
|
|
436
439
|
defaultTaskRegistry.maybeGc();
|
|
437
440
|
const forgetOnThrow = () => forgetQuietly(sessions, sessionId);
|
|
438
441
|
const extraBodyCollisions = reservedCollisions(model.extraBody, reservedFor(model.api));
|
|
@@ -2743,6 +2746,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2743
2746
|
}
|
|
2744
2747
|
}
|
|
2745
2748
|
: undefined;
|
|
2749
|
+
const notifyHookError = (err) => {
|
|
2750
|
+
try {
|
|
2751
|
+
deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId });
|
|
2752
|
+
}
|
|
2753
|
+
catch {
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2746
2756
|
const checkpointStore = spec.checkpointStore ?? deps.checkpointStore;
|
|
2747
2757
|
const durableApproval = spec.durableApproval ??
|
|
2748
2758
|
(runtimeCaps?.forceDurableGate ? { scope: spec.principal || DEFAULT_IRREVERSIBLE_SCOPE } : undefined);
|
|
@@ -3183,6 +3193,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3183
3193
|
reversibilityProbe: reversibilityProbes.get(e.toolName),
|
|
3184
3194
|
abortSignal: abortController.signal,
|
|
3185
3195
|
...(notifyPermissionDenied ? { permissionDenied: notifyPermissionDenied } : {}),
|
|
3196
|
+
onHookError: notifyHookError,
|
|
3186
3197
|
shellGated: (e.toolName === "Bash" && shellGatedBash) || (e.toolName === "Monitor" && shellGatedMonitor),
|
|
3187
3198
|
...(autoModeDecider ? { autoMode: { decider: autoModeDecider } } : {}),
|
|
3188
3199
|
isMarkedUnresolvable: (toolCallId) => inheritedUnavailableAsks.has(toolCallId),
|
|
@@ -341,8 +341,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
341
341
|
const scan = await Promise.race([
|
|
342
342
|
prepared.detectExternalChanges(maxFiles),
|
|
343
343
|
new Promise((resolve) => {
|
|
344
|
-
|
|
345
|
-
t.unref?.();
|
|
344
|
+
setTimeout(() => resolve({ changed: [], evicted: [] }), SCAN_CEILING_MS);
|
|
346
345
|
}),
|
|
347
346
|
]);
|
|
348
347
|
for (const p of scan.evicted)
|
|
@@ -475,8 +474,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
475
474
|
probed = await Promise.race([
|
|
476
475
|
rs.attach.instrProbe(prepared.instructionSources),
|
|
477
476
|
new Promise((resolve) => {
|
|
478
|
-
|
|
479
|
-
t.unref?.();
|
|
477
|
+
setTimeout(() => resolve(null), PROBE_CEILING_MS);
|
|
480
478
|
}),
|
|
481
479
|
]);
|
|
482
480
|
}
|
|
@@ -811,16 +809,18 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
811
809
|
rs.turn.turnUsageMissing = true;
|
|
812
810
|
const u = m.usage;
|
|
813
811
|
stats.tokens += u.totalTokens || 0;
|
|
814
|
-
const { totalInputTokens:
|
|
812
|
+
const { totalInputTokens: turnTotal, uncachedInputTokens: turnUncached, costMicroUsd: turnCostMicroUsd, } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
|
|
815
813
|
if (!rs.telemetry.pricingConfigured)
|
|
816
814
|
rs.telemetry.unpricedSpend = true;
|
|
817
|
-
stats.promptTokens +=
|
|
815
|
+
stats.promptTokens += turnUncached;
|
|
816
|
+
stats.totalInputTokens += turnTotal;
|
|
818
817
|
stats.cachedTokens += u.cacheRead || 0;
|
|
819
818
|
stats.cacheWriteTokens += u.cacheWrite || 0;
|
|
820
819
|
stats.outputTokens += u.output || 0;
|
|
821
820
|
stats.costMicroUsd += turnCostMicroUsd;
|
|
822
|
-
rs.turn.turnUsage ??= { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
|
|
823
|
-
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;
|
|
824
824
|
rs.turn.turnUsage.outputTokens += u.output || 0;
|
|
825
825
|
rs.turn.turnUsage.cacheReadTokens += u.cacheRead || 0;
|
|
826
826
|
rs.turn.turnUsage.cacheWriteTokens += u.cacheWrite || 0;
|
|
@@ -846,7 +846,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
846
846
|
model: m.model,
|
|
847
847
|
provider: m.provider,
|
|
848
848
|
turn: stats.turns + 1,
|
|
849
|
-
promptTokens:
|
|
849
|
+
promptTokens: turnUncached,
|
|
850
|
+
totalInputTokens: turnTotal,
|
|
850
851
|
completionTokens: u.output || 0,
|
|
851
852
|
cacheRead: u.cacheRead || 0,
|
|
852
853
|
cacheWrite: u.cacheWrite || 0,
|
|
@@ -1262,7 +1263,6 @@ export class Runner {
|
|
|
1262
1263
|
let t;
|
|
1263
1264
|
const timeout = new Promise((res2) => {
|
|
1264
1265
|
t = setTimeout(() => res2(undefined), READY_TIMEOUT_MS);
|
|
1265
|
-
t.unref?.();
|
|
1266
1266
|
});
|
|
1267
1267
|
return Promise.race([p, timeout]).finally(() => clearTimeout(t));
|
|
1268
1268
|
};
|
|
@@ -1650,7 +1650,7 @@ export class Runner {
|
|
|
1650
1650
|
}
|
|
1651
1651
|
}
|
|
1652
1652
|
onReady({ harness: prepared.harness, abortController: prepared.abortController });
|
|
1653
|
-
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 };
|
|
1654
1654
|
prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
|
|
1655
1655
|
if (resume &&
|
|
1656
1656
|
resume.cp.suspendedAt !== undefined &&
|
|
@@ -2132,9 +2132,10 @@ export class Runner {
|
|
|
2132
2132
|
const priced = this.deps.pricing?.[m.id] !== undefined || m.cost !== undefined;
|
|
2133
2133
|
if (!priced)
|
|
2134
2134
|
rs.telemetry.unpricedSpend = true;
|
|
2135
|
-
const { totalInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
|
|
2135
|
+
const { totalInputTokens, uncachedInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
|
|
2136
2136
|
stats.tokens += u.totalTokens || 0;
|
|
2137
|
-
stats.promptTokens +=
|
|
2137
|
+
stats.promptTokens += uncachedInputTokens;
|
|
2138
|
+
stats.totalInputTokens += totalInputTokens;
|
|
2138
2139
|
stats.cachedTokens += u.cacheRead || 0;
|
|
2139
2140
|
stats.cacheWriteTokens += u.cacheWrite || 0;
|
|
2140
2141
|
stats.outputTokens += u.output || 0;
|
|
@@ -2142,7 +2143,7 @@ export class Runner {
|
|
|
2142
2143
|
stats.compactionMicroUsd = (stats.compactionMicroUsd ?? 0) + costMicroUsd;
|
|
2143
2144
|
emitTrace(rs.telemetry.tracer, () => ({
|
|
2144
2145
|
kind: "brain.call", version: 1, taskId: rs.telemetry.taskId, model: m.id, provider: m.provider,
|
|
2145
|
-
promptTokens: totalInputTokens, completionTokens: u.output || 0,
|
|
2146
|
+
promptTokens: uncachedInputTokens, totalInputTokens, completionTokens: u.output || 0,
|
|
2146
2147
|
cacheRead: u.cacheRead || 0, cacheWrite: u.cacheWrite || 0,
|
|
2147
2148
|
latencyMs: 0, ...(priced ? { costMicroUsd } : {}), ...(typeof msg?.stopReason === "string" ? { stopReason: msg.stopReason } : {}), ts: Date.now(),
|
|
2148
2149
|
}));
|
|
@@ -2741,17 +2742,17 @@ export class Runner {
|
|
|
2741
2742
|
costMicroUsd: prepared.nestedStats.costMicroUsd || undefined,
|
|
2742
2743
|
};
|
|
2743
2744
|
}
|
|
2744
|
-
if (stats.
|
|
2745
|
-
const rawHit = stats.cachedTokens / stats.
|
|
2746
|
-
if (stats.cachedTokens > stats.
|
|
2747
|
-
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 });
|
|
2748
2749
|
}
|
|
2749
2750
|
stats.cacheHitRate = Math.min(1, rawHit);
|
|
2750
|
-
if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.
|
|
2751
|
+
if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.totalInputTokens >= 8000 && stats.cacheHitRate < 0.15) {
|
|
2751
2752
|
const cause = rs.degrade.degraded
|
|
2752
2753
|
? `a mid-task model switch (${rs.degrade.degraded.from} → ${rs.degrade.degraded.to}, degradation) reset the prefix cache — this is the likely cause`
|
|
2753
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`;
|
|
2754
|
-
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 });
|
|
2755
2756
|
}
|
|
2756
2757
|
}
|
|
2757
2758
|
if (prepared.resourceLedger &&
|
|
@@ -3035,7 +3036,6 @@ export class Runner {
|
|
|
3035
3036
|
ac.abort();
|
|
3036
3037
|
resolve([]);
|
|
3037
3038
|
}, SUGGESTIONS_TIMEOUT_MS);
|
|
3038
|
-
timer.unref?.();
|
|
3039
3039
|
});
|
|
3040
3040
|
const work = (async () => {
|
|
3041
3041
|
let model = prepared.model;
|
|
@@ -3107,7 +3107,6 @@ export class Runner {
|
|
|
3107
3107
|
ac.abort();
|
|
3108
3108
|
resolve({ ok: false, error: { code: "aborted", message: "snapshot timed out" } });
|
|
3109
3109
|
}, SNAPSHOT_TIMEOUT_MS);
|
|
3110
|
-
timer.unref?.();
|
|
3111
3110
|
});
|
|
3112
3111
|
const work = store.snapshot(prepared.sessionId, leafId, env, root, ac.signal);
|
|
3113
3112
|
work.catch(() => { });
|
|
@@ -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
|
}
|
|
@@ -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) {
|