@sema-agent/core 1.451.0 → 1.452.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/observer.js +3 -0
- package/dist/agents/subagent.js +51 -12
- package/dist/core/ask-question.js +5 -5
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +5 -1
- package/dist/core/exec-output-tail.d.ts +18 -1
- package/dist/core/exec-output-tail.js +38 -5
- package/dist/core/lsp-session.d.ts +1 -0
- package/dist/core/lsp-session.js +24 -6
- package/dist/core/lsp.d.ts +10 -0
- package/dist/core/lsp.js +63 -6
- package/dist/core/mailbox-store.d.ts +3 -2
- package/dist/core/mailbox-store.js +19 -4
- package/dist/core/memory.js +1 -1
- package/dist/core/runner/prepare-task.js +10 -1
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/session-reconcile.js +5 -2
- package/dist/core/task-notification.d.ts +1 -0
- package/dist/core/task-registry.d.ts +15 -9
- package/dist/core/task-registry.js +290 -53
- package/dist/core/tool-result-store.js +2 -2
- package/dist/core/tools.d.ts +5 -0
- package/dist/core/tools.js +3 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/core/workflow-journal-store.d.ts +2 -0
- package/dist/core/workflow-journal-store.js +14 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +1 -0
- package/dist/engine/execution-env/node-execution-env.js +130 -20
- package/dist/engine/lsp/node-lsp-manager.d.ts +3 -1
- package/dist/engine/lsp/node-lsp-manager.js +22 -5
- package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
- package/dist/engine/lsp/stdio-lsp-transport.js +17 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +1 -0
- package/dist/orchestration/run-workflow-tool.js +29 -6
- package/dist/orchestration/workflow.d.ts +9 -0
- package/dist/orchestration/workflow.js +80 -5
- package/dist/stores/cc/mailbox-store.js +6 -1
- package/dist/stores/file/background-agent-store.js +3 -2
- package/dist/stores/file/mailbox-store.d.ts +1 -1
- package/dist/stores/file/mailbox-store.js +9 -7
- package/dist/tools/fs/encoding.d.ts +5 -0
- package/dist/tools/fs/encoding.js +6 -0
- package/dist/tools/fs/index.js +184 -120
- package/dist/tools/fs/notebook.d.ts +43 -0
- package/dist/tools/fs/notebook.js +141 -0
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/search.js +141 -12
- package/dist/tools/gitea-issue.js +4 -2
- package/dist/tools/monitor.js +12 -8
- package/dist/tools/scheduler-tools.js +16 -16
- package/dist/tools/task-list.js +34 -12
- package/dist/tools/web.d.ts +2 -0
- package/dist/tools/web.js +105 -19
- package/dist/tools/worktree.js +14 -14
- package/package.json +1 -1
|
@@ -93,6 +93,15 @@ export interface WorkflowRun {
|
|
|
93
93
|
error?: string;
|
|
94
94
|
result?: string;
|
|
95
95
|
resultFull?: string;
|
|
96
|
+
completionId?: string;
|
|
97
|
+
resume?: {
|
|
98
|
+
fromRunId: string;
|
|
99
|
+
journalEntries: number;
|
|
100
|
+
replayed: number;
|
|
101
|
+
divergedAtOrdinal?: number;
|
|
102
|
+
divergedReason?: string;
|
|
103
|
+
};
|
|
104
|
+
journalSkips?: number;
|
|
96
105
|
}
|
|
97
106
|
export type WorkflowEvent = {
|
|
98
107
|
type: "run_start";
|
|
@@ -2,10 +2,11 @@ import { resolveModelDisplayLabel } from "../core/roles.js";
|
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { availableParallelism } from "node:os";
|
|
5
|
+
import { uuidv7 } from "../internal/harness.js";
|
|
5
6
|
import { builtinAgentDefinitions } from "../agents/builtin-agents.js";
|
|
6
7
|
import { GENERAL_PURPOSE_SUBAGENT_TYPE } from "../agents/subagent.js";
|
|
7
8
|
import { combinePolicies, createAllowDenyPolicy } from "../core/tool-policy.js";
|
|
8
|
-
import { callKeyOrdinal } from "../core/workflow-journal-store.js";
|
|
9
|
+
import { callKeyOrdinal, oversizeJournalResult, journalOversizeTombstone, JOURNAL_OVERSIZE_ERROR_CODE, MAX_JOURNAL_RESULT_BYTES } from "../core/workflow-journal-store.js";
|
|
9
10
|
import { isWorkflowRunActive, closeWorkflowChannel, markWorkflowActive, publishWorkflowEvent } from "./workflow-observe.js";
|
|
10
11
|
import { isDurablePause, mapNestedSuspend } from "../agents/suspend-guard.js";
|
|
11
12
|
import { boundInputHashOf } from "../core/canonical-json.js";
|
|
@@ -551,13 +552,39 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
551
552
|
});
|
|
552
553
|
};
|
|
553
554
|
let journalTail = Promise.resolve();
|
|
554
|
-
const journalAppend = async (callKey, result) => {
|
|
555
|
+
const journalAppend = async (callKey, result, label) => {
|
|
555
556
|
const dbg = typeof process !== "undefined" && process.env?.SEMA_DEBUG_WORKFLOW_JOURNAL === "1";
|
|
556
557
|
if (!journalStore) {
|
|
557
558
|
if (dbg)
|
|
558
559
|
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} SKIP (no journalStore on this run)`);
|
|
559
560
|
return;
|
|
560
561
|
}
|
|
562
|
+
let serialized;
|
|
563
|
+
try {
|
|
564
|
+
serialized = JSON.stringify(result);
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
serialized = undefined;
|
|
568
|
+
}
|
|
569
|
+
if (serialized !== undefined && oversizeJournalResult(serialized)) {
|
|
570
|
+
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
571
|
+
if (!finalized)
|
|
572
|
+
run.journalSkips = (run.journalSkips ?? 0) + 1;
|
|
573
|
+
emitRunLog(`resume-journal: agent #${callKeyOrdinal(callKey)}${label !== undefined ? ` "${label.slice(0, 80)}"` : ""} result is ${bytes} bytes, ` +
|
|
574
|
+
`over the ${MAX_JOURNAL_RESULT_BYTES}-byte per-entry cap — NOT cached. A resume from this run re-runs this agent and everything after it live.`);
|
|
575
|
+
const tombstone = journalStore.append(runId, scope, { callKey, result: journalOversizeTombstone(result, bytes) });
|
|
576
|
+
journalTail = journalTail.then(() => tombstone).catch(() => undefined);
|
|
577
|
+
try {
|
|
578
|
+
await tombstone;
|
|
579
|
+
if (dbg)
|
|
580
|
+
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} OVERSIZE-TOMBSTONE (${bytes} bytes)`);
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
if (dbg)
|
|
584
|
+
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} TOMBSTONE-ERROR ${err instanceof Error ? err.message : String(err)}`);
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
561
588
|
const p = journalStore.append(runId, scope, { callKey, result });
|
|
562
589
|
journalTail = journalTail.then(() => p).catch(() => undefined);
|
|
563
590
|
try {
|
|
@@ -623,6 +650,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
623
650
|
: message;
|
|
624
651
|
emit({ type: "log", runId, message: capped, ts: now() });
|
|
625
652
|
};
|
|
653
|
+
let divergenceNoted = false;
|
|
654
|
+
const noteDivergence = (ordinal, reason) => {
|
|
655
|
+
if (opts.resumeFromRunId === undefined || divergenceNoted)
|
|
656
|
+
return;
|
|
657
|
+
divergenceNoted = true;
|
|
658
|
+
if (run.resume) {
|
|
659
|
+
run.resume.divergedAtOrdinal = ordinal;
|
|
660
|
+
run.resume.divergedReason = reason;
|
|
661
|
+
}
|
|
662
|
+
emitRunLog(`resume: replay stopped at agent #${ordinal} (${reason}) — this call and every later call run live.`);
|
|
663
|
+
};
|
|
626
664
|
const settleAgentError = (rec, result, label, rawOutput) => {
|
|
627
665
|
if (result.status !== "completed") {
|
|
628
666
|
if (result.errorCode !== undefined)
|
|
@@ -695,6 +733,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
695
733
|
...(rs.toolCalls !== undefined ? { toolCalls: rs.toolCalls } : {}),
|
|
696
734
|
output: cachedOutput,
|
|
697
735
|
};
|
|
736
|
+
if (run.resume)
|
|
737
|
+
run.resume.replayed += 1;
|
|
698
738
|
run.agents.push(replayRec);
|
|
699
739
|
if (phaseInstance)
|
|
700
740
|
agentPhaseOf.set(replayRec, phaseInstance);
|
|
@@ -710,6 +750,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
710
750
|
}
|
|
711
751
|
return r;
|
|
712
752
|
}
|
|
753
|
+
noteDivergence(run.agents.length, cached === undefined
|
|
754
|
+
? run.resume?.journalEntries === 0
|
|
755
|
+
? "the journal loaded no entries"
|
|
756
|
+
: run.agents.length >= replayByOrdinal.length
|
|
757
|
+
? "a new call past the journaled prefix — the prior run ended before this point (the intended extend-the-script resume shape)"
|
|
758
|
+
: "no journal entry at this ordinal (never recorded, e.g. an oversize result under a pre-tombstone engine, or dropped)"
|
|
759
|
+
: cached.callKey !== callKey
|
|
760
|
+
? "the call key changed — the script or its args differ here"
|
|
761
|
+
: cached.result.errorCode === JOURNAL_OVERSIZE_ERROR_CODE
|
|
762
|
+
? "the prior result exceeded the journal size cap and was never cached"
|
|
763
|
+
: `the journaled result was ${cached.result.status}, not completed`);
|
|
713
764
|
diverged = true;
|
|
714
765
|
}
|
|
715
766
|
if (budgetTotal !== null && spent() >= budgetTotal) {
|
|
@@ -963,7 +1014,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
963
1014
|
status: "failed",
|
|
964
1015
|
result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
|
|
965
1016
|
stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
|
|
966
|
-
}).catch(() => undefined);
|
|
1017
|
+
}, label).catch(() => undefined);
|
|
967
1018
|
bceTerminal(callKey, "failed", rec.output ?? (err instanceof Error ? err.message : String(err)), rec.sessionId, rec.stats);
|
|
968
1019
|
}
|
|
969
1020
|
throw err;
|
|
@@ -992,7 +1043,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
992
1043
|
emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: rec.status, output, ...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}), ...(result.stats.toolCalls !== undefined ? { toolCalls: result.stats.toolCalls } : {}), ts: rec.endedAt });
|
|
993
1044
|
void persist("update");
|
|
994
1045
|
bceTerminal(callKey, rec.status === "completed" ? "completed" : "failed", output, result.sessionId || undefined, rec.stats);
|
|
995
|
-
await journalAppend(callKey, result);
|
|
1046
|
+
await journalAppend(callKey, result, label);
|
|
996
1047
|
if (agentOpts.schema && result.status === "completed" && result.structuredOutput === undefined) {
|
|
997
1048
|
throw new WorkflowAgentSchemaError(label, result);
|
|
998
1049
|
}
|
|
@@ -1026,6 +1077,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1026
1077
|
const callKey = workflowAgentCallKey(run.agents.length, specForIdentity, agentOpts);
|
|
1027
1078
|
const prompt = boundedRedactedSummary(spec.systemPrompt ? `${spec.systemPrompt}\n\n${spec.objective}` : spec.objective, MAX_TRANSCRIPT_CHARS);
|
|
1028
1079
|
const model = workflowModelLabel(specForIdentity);
|
|
1080
|
+
noteDivergence(run.agents.length, "ctx.agentStream results are never replayed");
|
|
1029
1081
|
diverged = true;
|
|
1030
1082
|
const { tail: activityTail, onActivity } = makeActivityCapture(callKey, label, groupId);
|
|
1031
1083
|
const rec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(model !== undefined ? { model } : {}), status: "running", queuedAt: now() };
|
|
@@ -1189,7 +1241,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1189
1241
|
emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: rec.status, output, ...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}), ...(result.stats.toolCalls !== undefined ? { toolCalls: result.stats.toolCalls } : {}), ts: rec.endedAt });
|
|
1190
1242
|
void persist("update");
|
|
1191
1243
|
bceTerminal(callKey, rec.status === "completed" ? "completed" : "failed", output, result.sessionId || undefined, rec.stats);
|
|
1192
|
-
await journalAppend(callKey, result).catch(() => undefined);
|
|
1244
|
+
await journalAppend(callKey, result, label).catch(() => undefined);
|
|
1193
1245
|
if (agentOpts.schema && result.status === "completed" && result.structuredOutput === undefined) {
|
|
1194
1246
|
throw new WorkflowAgentSchemaError(label, result);
|
|
1195
1247
|
}
|
|
@@ -1239,6 +1291,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1239
1291
|
const fanOutOpts = trailing !== undefined && typeof trailing !== "function" ? trailing : undefined;
|
|
1240
1292
|
const stages = (fanOutOpts !== undefined ? stagesAndOpts.slice(0, -1) : stagesAndOpts);
|
|
1241
1293
|
const errorsOut = fanOutOpts?.errors;
|
|
1294
|
+
noteDivergence(run.agents.length, "ctx.pipeline ordinals are latency-dependent, so its calls always run live on a resume");
|
|
1242
1295
|
diverged = true;
|
|
1243
1296
|
const settled = await Promise.all(items.map((item, index) => stages
|
|
1244
1297
|
.reduce((acc, stage) => acc.then((prev) => stage(prev, item, index)), Promise.resolve(item))
|
|
@@ -1370,6 +1423,19 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1370
1423
|
const entries = await journalStore.load(opts.resumeFromRunId, scope);
|
|
1371
1424
|
for (const e of entries)
|
|
1372
1425
|
replayByOrdinal[callKeyOrdinal(e.callKey)] = e;
|
|
1426
|
+
run.resume = {
|
|
1427
|
+
fromRunId: opts.resumeFromRunId.replace(/[^A-Za-z0-9_.:-]/g, "").slice(0, 64),
|
|
1428
|
+
journalEntries: entries.length,
|
|
1429
|
+
replayed: 0,
|
|
1430
|
+
};
|
|
1431
|
+
if (entries.length === 0) {
|
|
1432
|
+
emitRunLog("resume: the prior run's journal yielded NO entries under this run's scope — nothing will replay; the entire script runs live. " +
|
|
1433
|
+
"The prior run id may be unknown or pruned, recorded under a different scope, or its journal unreadable.");
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
emitRunLog(`resume: loaded ${entries.length} journaled result${entries.length === 1 ? "" : "s"} — the longest unchanged prefix replays; the first changed/new call and everything after run live.`);
|
|
1437
|
+
}
|
|
1438
|
+
void persist("update");
|
|
1373
1439
|
}
|
|
1374
1440
|
return fn(ctx);
|
|
1375
1441
|
};
|
|
@@ -1416,6 +1482,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1416
1482
|
if (run.result !== fullResult)
|
|
1417
1483
|
run.resultFull = fullResult;
|
|
1418
1484
|
run.status = "completed";
|
|
1485
|
+
run.completionId ??= uuidv7();
|
|
1419
1486
|
run.endedAt = now();
|
|
1420
1487
|
closeAbandonedAgents(run.endedAt);
|
|
1421
1488
|
restampPhaseFailures();
|
|
@@ -1431,6 +1498,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1431
1498
|
settleActiveUsageBeats();
|
|
1432
1499
|
closeOpenMarker("failed");
|
|
1433
1500
|
run.status = "failed";
|
|
1501
|
+
run.completionId ??= uuidv7();
|
|
1434
1502
|
run.endedAt = now();
|
|
1435
1503
|
run.error = err instanceof Error ? err.message : String(err);
|
|
1436
1504
|
closeAbandonedAgents(run.endedAt);
|
|
@@ -1440,6 +1508,13 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1440
1508
|
run.agentFailures = failedRunFailures;
|
|
1441
1509
|
emit({ type: "run_end", runId, status: "failed", ...(failedRunFailures > 0 ? { agentFailures: failedRunFailures } : {}), ts: run.endedAt });
|
|
1442
1510
|
await persist("update");
|
|
1511
|
+
if (err !== null && (typeof err === "object" || typeof err === "function")) {
|
|
1512
|
+
try {
|
|
1513
|
+
Object.defineProperty(err, "workflowRun", { value: run, enumerable: false, configurable: true });
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1443
1518
|
throw err;
|
|
1444
1519
|
}
|
|
1445
1520
|
finally {
|
|
@@ -89,9 +89,12 @@ export function createCcFileMailboxStore(opts) {
|
|
|
89
89
|
leases.set(key, { owner, expiresAt: t + ttlMs, maxSeq });
|
|
90
90
|
return Promise.resolve({ maxSeq, messages: pending });
|
|
91
91
|
},
|
|
92
|
-
ack: (scope, handle, upToSeq) => {
|
|
92
|
+
ack: (scope, handle, owner, upToSeq) => {
|
|
93
93
|
requireDefaultScope(scope);
|
|
94
94
|
const path = inboxPath(handle);
|
|
95
|
+
const held = leases.get(path);
|
|
96
|
+
if (held === undefined || held.owner !== owner)
|
|
97
|
+
return Promise.resolve();
|
|
95
98
|
if (!existsSync(path))
|
|
96
99
|
return Promise.resolve();
|
|
97
100
|
return withCcLock(path, () => {
|
|
@@ -105,6 +108,8 @@ export function createCcFileMailboxStore(opts) {
|
|
|
105
108
|
}
|
|
106
109
|
if (changed)
|
|
107
110
|
saveBox(path, box);
|
|
111
|
+
if (held.maxSeq <= upToSeq)
|
|
112
|
+
leases.delete(path);
|
|
108
113
|
});
|
|
109
114
|
},
|
|
110
115
|
releaseLease: (scope, handle, owner) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { BackgroundAgentStoreError, } from "../../core/background-agent-store.js";
|
|
2
|
+
import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, } from "../../core/background-agent-store.js";
|
|
3
3
|
import { canonicalStoreKey, AppendLog, atomicWriteFile, ensureDir, readJsonlRecords } from "./fs-atomic.js";
|
|
4
4
|
const sharedAgentDirs = new Map();
|
|
5
5
|
export class FileBackgroundAgentStore {
|
|
@@ -205,7 +205,8 @@ export class FileBackgroundAgentStore {
|
|
|
205
205
|
const next = structuredClone(live);
|
|
206
206
|
next.status = "failed";
|
|
207
207
|
next.stoppedBy = "system";
|
|
208
|
-
next.summary = next.summary ??
|
|
208
|
+
next.summary = next.summary ?? STALE_RUNNING_REAP_ATTRIBUTION;
|
|
209
|
+
next.error = next.error ?? STALE_RUNNING_REAP_ATTRIBUTION;
|
|
209
210
|
next.settledAt = now;
|
|
210
211
|
next.updatedAt = now;
|
|
211
212
|
next.rev = live.rev + 1;
|
|
@@ -21,7 +21,7 @@ export declare class FileMailboxStore implements MailboxStore {
|
|
|
21
21
|
sentAt: number;
|
|
22
22
|
}): Promise<number>;
|
|
23
23
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
24
|
-
ack(scope: string, handle: string, upToSeq: number): Promise<void>;
|
|
24
|
+
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
|
|
25
25
|
releaseLease(scope: string, handle: string, owner: string): Promise<void>;
|
|
26
26
|
peekCount(scope: string, handle: string): Promise<number>;
|
|
27
27
|
drop(scope: string, handle: string): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, resolve, sep } from "node:path";
|
|
2
2
|
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
-
import {} from "../../core/mailbox-store.js";
|
|
3
|
+
import { newestSentAt, } from "../../core/mailbox-store.js";
|
|
4
4
|
import { AppendLog, atomicWriteFile, canonicalStoreKey, ensureDir, readJsonlRecords, sanitizeScope, sanitizePathComponent } from "./fs-atomic.js";
|
|
5
5
|
function realpathSyncSafe(p) {
|
|
6
6
|
try {
|
|
@@ -86,7 +86,7 @@ export class FileMailboxStore {
|
|
|
86
86
|
lines.push(JSON.stringify({ t: "lease", ...b.lease }));
|
|
87
87
|
b.log.closeForSwap();
|
|
88
88
|
atomicWriteFile(this.tmpDir, path, lines.length ? `${lines.join("\n")}\n` : "");
|
|
89
|
-
b.events =
|
|
89
|
+
b.events = 0;
|
|
90
90
|
}
|
|
91
91
|
async append(scope, handle, msg) {
|
|
92
92
|
if (scope === undefined || scope === "")
|
|
@@ -111,9 +111,11 @@ export class FileMailboxStore {
|
|
|
111
111
|
return { messages: b.messages.map((m) => ({ ...m })), maxSeq };
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
|
-
async ack(scope, handle, upToSeq) {
|
|
114
|
+
async ack(scope, handle, owner, upToSeq) {
|
|
115
115
|
return withPathLock(this.lockKey(scope, handle), () => {
|
|
116
116
|
const b = this.load(scope, handle);
|
|
117
|
+
if (b.lease === undefined || b.lease.owner !== owner)
|
|
118
|
+
return;
|
|
117
119
|
this.commit(b, this.boxPath(scope, handle), { t: "ack", upToSeq });
|
|
118
120
|
});
|
|
119
121
|
}
|
|
@@ -160,15 +162,15 @@ export class FileMailboxStore {
|
|
|
160
162
|
for (const [key, b] of [...sharedBoxes]) {
|
|
161
163
|
if (!b.path.startsWith(prefix))
|
|
162
164
|
continue;
|
|
163
|
-
const seen = b.messages
|
|
164
|
-
if (seen === undefined || seen
|
|
165
|
+
const seen = newestSentAt(b.messages);
|
|
166
|
+
if (seen === undefined || seen >= now - maxAgeMs)
|
|
165
167
|
continue;
|
|
166
168
|
const swept = await withPathLock(key, () => {
|
|
167
169
|
const live = sharedBoxes.get(key);
|
|
168
170
|
if (live === undefined || live !== b)
|
|
169
171
|
return false;
|
|
170
|
-
const newest = b.messages
|
|
171
|
-
if (newest === undefined || newest
|
|
172
|
+
const newest = newestSentAt(b.messages);
|
|
173
|
+
if (newest === undefined || newest >= now - maxAgeMs)
|
|
172
174
|
return false;
|
|
173
175
|
b.messages = [];
|
|
174
176
|
delete b.lease;
|
|
@@ -13,3 +13,8 @@ export declare function detectFileEncoding(bytes: Uint8Array): DetectedFileEncod
|
|
|
13
13
|
export declare function decodeTextBytes(bytes: Uint8Array): DecodedTextFile;
|
|
14
14
|
export declare function encodeTextForFile(text: string, encoding: DetectedFileEncoding, endings: DetectedLineEndings | "preserve"): string | Uint8Array;
|
|
15
15
|
export declare function normalizeEditText(s: string): string;
|
|
16
|
+
export declare function splitLeadingBom(text: string): {
|
|
17
|
+
hadBom: boolean;
|
|
18
|
+
text: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function normalizeFileText(s: string): string;
|
|
@@ -41,3 +41,9 @@ export function encodeTextForFile(text, encoding, endings) {
|
|
|
41
41
|
export function normalizeEditText(s) {
|
|
42
42
|
return s.replaceAll("\r\n", "\n");
|
|
43
43
|
}
|
|
44
|
+
export function splitLeadingBom(text) {
|
|
45
|
+
return text.charCodeAt(0) === 0xfeff ? { hadBom: true, text: text.slice(1) } : { hadBom: false, text };
|
|
46
|
+
}
|
|
47
|
+
export function normalizeFileText(s) {
|
|
48
|
+
return normalizeEditText(splitLeadingBom(s).text);
|
|
49
|
+
}
|