@hyperdrive.bot/fleet-server 0.3.159 → 0.3.161
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/server/server/agent/agent-manager.d.ts +29 -0
- package/dist/server/server/agent/agent-manager.js +69 -5
- package/dist/server/server/agent/agent-timeline-store.d.ts +10 -0
- package/dist/server/server/agent/agent-timeline-store.js +54 -6
- package/dist/server/server/agent/blocker-log.d.ts +3 -1
- package/dist/server/server/agent/blocker-log.js +17 -5
- package/dist/server/server/agent/card-move-log.d.ts +3 -1
- package/dist/server/server/agent/card-move-log.js +21 -14
- package/dist/server/server/agent/import-sessions.js +1 -1
- package/dist/server/server/agent/jsonl-card-index.d.ts +53 -0
- package/dist/server/server/agent/jsonl-card-index.js +138 -0
- package/dist/server/server/agent/lifecycle-command.d.ts +18 -1
- package/dist/server/server/agent/lifecycle-command.js +21 -2
- package/dist/server/server/agent/mcp-shared.d.ts +1 -0
- package/dist/server/server/agent/provider-snapshot-manager.d.ts +9 -0
- package/dist/server/server/agent/provider-snapshot-manager.js +38 -1
- package/dist/server/server/agent/structured-generation-providers.d.ts +2 -1
- package/dist/server/server/agent/structured-generation-providers.js +21 -8
- package/dist/server/server/fleet/decision-service.d.ts +8 -0
- package/dist/server/server/fleet/decision-service.js +24 -1
- package/dist/server/server/fleet/fleet-controls.js +2 -1
- package/dist/server/server/fleet/fleet-reader.d.ts +12 -0
- package/dist/server/server/fleet/fleet-reader.js +48 -2
- package/dist/server/server/schedule/service.d.ts +4 -0
- package/dist/server/server/schedule/service.js +26 -5
- package/dist/server/server/schedule/store.d.ts +51 -0
- package/dist/server/server/schedule/store.js +184 -16
- package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +16 -1
- package/dist/server/server/session/agent-updates/agent-updates-service.js +59 -2
- package/dist/server/server/session/provider/provider-catalog-session.d.ts +8 -0
- package/dist/server/server/session/provider/provider-catalog-session.js +21 -1
- package/dist/server/server/session.d.ts +16 -0
- package/dist/server/server/session.js +115 -19
- package/dist/server/server/websocket-server.d.ts +5 -0
- package/dist/server/server/websocket-server.js +10 -0
- package/dist/server/server/workspace-directory.d.ts +4 -1
- package/dist/server/server/workspace-directory.js +3 -2
- package/dist/server/web-ui/_expo/static/js/web/{index-1420ca2d21c343afea2c8e2da752cd54.js → index-6a8adbad019c19e5bd71cf06dc31528b.js} +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-6a8adbad019c19e5bd71cf06dc31528b.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-6a8adbad019c19e5bd71cf06dc31528b.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-1420ca2d21c343afea2c8e2da752cd54.js.map.br → index-6a8adbad019c19e5bd71cf06dc31528b.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-1420ca2d21c343afea2c8e2da752cd54.js.map.gz → index-6a8adbad019c19e5bd71cf06dc31528b.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-1420ca2d21c343afea2c8e2da752cd54.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-1420ca2d21c343afea2c8e2da752cd54.js.gz +0 -0
|
@@ -236,6 +236,8 @@ export declare class AgentManager {
|
|
|
236
236
|
private readonly providerSwapMetadata;
|
|
237
237
|
private readonly agents;
|
|
238
238
|
private readonly timelineStore;
|
|
239
|
+
/** Deferred archive teardowns in flight, per agent. See `scheduleArchiveTeardown`. */
|
|
240
|
+
private readonly archiveTeardowns;
|
|
239
241
|
private readonly agentsAwaitingInitialSnapshotPersist;
|
|
240
242
|
private readonly sessionEventTails;
|
|
241
243
|
private readonly foregroundRuns;
|
|
@@ -345,6 +347,8 @@ export declare class AgentManager {
|
|
|
345
347
|
listDraftFeatures(config: AgentSessionConfig): Promise<AgentFeature[]>;
|
|
346
348
|
getAgent(id: string): ManagedAgent | null;
|
|
347
349
|
getTimeline(id: string): AgentTimelineItem[];
|
|
350
|
+
/** Timeline row count, without the full copy `getTimeline(id).length` makes. */
|
|
351
|
+
getTimelineSize(id: string): number;
|
|
348
352
|
/**
|
|
349
353
|
* Timeline epoch for an agent. Row `seq` restarts at 1 whenever the store is
|
|
350
354
|
* re-initialized without seeded rows, so a seq is only comparable WITHIN an
|
|
@@ -414,6 +418,31 @@ export declare class AgentManager {
|
|
|
414
418
|
archiveAgent(agentId: string): Promise<{
|
|
415
419
|
archivedAt: string;
|
|
416
420
|
}>;
|
|
421
|
+
/**
|
|
422
|
+
* First half of `archiveAgent`: persist the archived record. After this the
|
|
423
|
+
* archive is durable and can be acknowledged; the process is still running.
|
|
424
|
+
*/
|
|
425
|
+
markLiveAgentArchived(agentId: string): Promise<{
|
|
426
|
+
archivedAt: string;
|
|
427
|
+
}>;
|
|
428
|
+
/**
|
|
429
|
+
* Second half of `archiveAgent`: close the process and archive the subagents
|
|
430
|
+
* it spawned. Safe to run after the archive was acknowledged.
|
|
431
|
+
*/
|
|
432
|
+
completeLiveAgentArchive(agentId: string): Promise<void>;
|
|
433
|
+
/**
|
|
434
|
+
* Run the deferred half of an archive (close the process if still open, archive
|
|
435
|
+
* subagents) in the background, serialized per agent.
|
|
436
|
+
*
|
|
437
|
+
* Everything that can revive the agent (unarchive, resume, reload) awaits it
|
|
438
|
+
* first via `awaitArchiveTeardown`, so the outcome is the same as the old
|
|
439
|
+
* synchronous archive: the revive always lands after the close, and a close
|
|
440
|
+
* can never land on a revived agent. Resolves to the error when the teardown
|
|
441
|
+
* failed, never rejects.
|
|
442
|
+
*/
|
|
443
|
+
scheduleArchiveTeardown(agentId: string): Promise<Error | null>;
|
|
444
|
+
/** Wait for a pending archive teardown of this agent, if any. */
|
|
445
|
+
awaitArchiveTeardown(agentId: string | undefined): Promise<void>;
|
|
417
446
|
private cascadeArchiveChildren;
|
|
418
447
|
private markRecordArchived;
|
|
419
448
|
private fireAgentArchived;
|
|
@@ -218,6 +218,8 @@ export class AgentManager {
|
|
|
218
218
|
this.providerSwapMetadata = new Map();
|
|
219
219
|
this.agents = new Map();
|
|
220
220
|
this.timelineStore = new InMemoryAgentTimelineStore();
|
|
221
|
+
/** Deferred archive teardowns in flight, per agent. See `scheduleArchiveTeardown`. */
|
|
222
|
+
this.archiveTeardowns = new Map();
|
|
221
223
|
this.agentsAwaitingInitialSnapshotPersist = new Set();
|
|
222
224
|
this.sessionEventTails = new Map();
|
|
223
225
|
this.foregroundRuns = new ForegroundRunState();
|
|
@@ -350,7 +352,7 @@ export class AgentManager {
|
|
|
350
352
|
if (!this.timelineStore.has(agent.id)) {
|
|
351
353
|
continue;
|
|
352
354
|
}
|
|
353
|
-
const len = this.timelineStore.
|
|
355
|
+
const len = this.timelineStore.size(agent.id);
|
|
354
356
|
totalItems += len;
|
|
355
357
|
if (len > maxItemsPerAgent) {
|
|
356
358
|
maxItemsPerAgent = len;
|
|
@@ -640,6 +642,11 @@ export class AgentManager {
|
|
|
640
642
|
this.requireAgent(id);
|
|
641
643
|
return this.timelineStore.getItems(id);
|
|
642
644
|
}
|
|
645
|
+
/** Timeline row count, without the full copy `getTimeline(id).length` makes. */
|
|
646
|
+
getTimelineSize(id) {
|
|
647
|
+
this.requireAgent(id);
|
|
648
|
+
return this.timelineStore.size(id);
|
|
649
|
+
}
|
|
643
650
|
/**
|
|
644
651
|
* Timeline epoch for an agent. Row `seq` restarts at 1 whenever the store is
|
|
645
652
|
* re-initialized without seeded rows, so a seq is only comparable WITHIN an
|
|
@@ -690,6 +697,7 @@ export class AgentManager {
|
|
|
690
697
|
// Reconstruct an agent from provider persistence. Callers should explicitly
|
|
691
698
|
// hydrate timeline history after resume.
|
|
692
699
|
async resumeAgentFromPersistence(handle, overrides, agentId, options) {
|
|
700
|
+
await this.awaitArchiveTeardown(agentId);
|
|
693
701
|
const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "resumeAgentFromPersistence");
|
|
694
702
|
const metadata = (handle.metadata ?? {});
|
|
695
703
|
const mergedConfig = {
|
|
@@ -853,6 +861,7 @@ export class AgentManager {
|
|
|
853
861
|
// Hot-reload an active agent session with config overrides while preserving
|
|
854
862
|
// in-memory timeline state.
|
|
855
863
|
async reloadAgentSession(agentId, overrides, options) {
|
|
864
|
+
await this.awaitArchiveTeardown(agentId);
|
|
856
865
|
let existing = this.requireSessionAgent(agentId);
|
|
857
866
|
if (this.hasInFlightRun(agentId)) {
|
|
858
867
|
await this.cancelAgentRun(agentId);
|
|
@@ -983,6 +992,15 @@ export class AgentManager {
|
|
|
983
992
|
}, "agent.manager.close.complete");
|
|
984
993
|
}
|
|
985
994
|
async archiveAgent(agentId) {
|
|
995
|
+
const { archivedAt } = await this.markLiveAgentArchived(agentId);
|
|
996
|
+
await this.completeLiveAgentArchive(agentId);
|
|
997
|
+
return { archivedAt };
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* First half of `archiveAgent`: persist the archived record. After this the
|
|
1001
|
+
* archive is durable and can be acknowledged; the process is still running.
|
|
1002
|
+
*/
|
|
1003
|
+
async markLiveAgentArchived(agentId) {
|
|
986
1004
|
const agent = this.requireAgent(agentId);
|
|
987
1005
|
if (!this.registry) {
|
|
988
1006
|
throw new Error("Agent storage is not configured");
|
|
@@ -996,10 +1014,54 @@ export class AgentManager {
|
|
|
996
1014
|
}
|
|
997
1015
|
const { archivedAt } = await this.markRecordArchived(stored);
|
|
998
1016
|
agent.updatedAt = new Date(archivedAt);
|
|
999
|
-
await this.closeAgent(agentId);
|
|
1000
|
-
await this.cascadeArchiveChildren(agentId);
|
|
1001
1017
|
return { archivedAt };
|
|
1002
1018
|
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Second half of `archiveAgent`: close the process and archive the subagents
|
|
1021
|
+
* it spawned. Safe to run after the archive was acknowledged.
|
|
1022
|
+
*/
|
|
1023
|
+
async completeLiveAgentArchive(agentId) {
|
|
1024
|
+
if (this.agents.has(agentId)) {
|
|
1025
|
+
await this.closeAgent(agentId);
|
|
1026
|
+
}
|
|
1027
|
+
await this.cascadeArchiveChildren(agentId);
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Run the deferred half of an archive (close the process if still open, archive
|
|
1031
|
+
* subagents) in the background, serialized per agent.
|
|
1032
|
+
*
|
|
1033
|
+
* Everything that can revive the agent (unarchive, resume, reload) awaits it
|
|
1034
|
+
* first via `awaitArchiveTeardown`, so the outcome is the same as the old
|
|
1035
|
+
* synchronous archive: the revive always lands after the close, and a close
|
|
1036
|
+
* can never land on a revived agent. Resolves to the error when the teardown
|
|
1037
|
+
* failed, never rejects.
|
|
1038
|
+
*/
|
|
1039
|
+
scheduleArchiveTeardown(agentId) {
|
|
1040
|
+
const previous = this.archiveTeardowns.get(agentId) ?? Promise.resolve();
|
|
1041
|
+
const run = previous
|
|
1042
|
+
.then(async () => {
|
|
1043
|
+
if (this.agents.has(agentId)) {
|
|
1044
|
+
await this.closeAgent(agentId);
|
|
1045
|
+
}
|
|
1046
|
+
await this.cascadeArchiveChildren(agentId);
|
|
1047
|
+
return null;
|
|
1048
|
+
})
|
|
1049
|
+
.catch((error) => (error instanceof Error ? error : new Error(String(error))));
|
|
1050
|
+
const tracked = run.then(() => undefined);
|
|
1051
|
+
this.archiveTeardowns.set(agentId, tracked);
|
|
1052
|
+
void tracked.finally(() => {
|
|
1053
|
+
if (this.archiveTeardowns.get(agentId) === tracked) {
|
|
1054
|
+
this.archiveTeardowns.delete(agentId);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
return run;
|
|
1058
|
+
}
|
|
1059
|
+
/** Wait for a pending archive teardown of this agent, if any. */
|
|
1060
|
+
async awaitArchiveTeardown(agentId) {
|
|
1061
|
+
if (!agentId)
|
|
1062
|
+
return;
|
|
1063
|
+
await this.archiveTeardowns.get(agentId);
|
|
1064
|
+
}
|
|
1003
1065
|
// Children created via the MCP `create_agent` tool carry the parent-agent-id
|
|
1004
1066
|
// label pointing back at the caller. Archiving the parent cascades to those
|
|
1005
1067
|
// children so subagent fleets don't outlive their orchestrator. Detached
|
|
@@ -1385,6 +1447,9 @@ export class AgentManager {
|
|
|
1385
1447
|
return nextRecord;
|
|
1386
1448
|
}
|
|
1387
1449
|
async unarchiveSnapshot(agentId) {
|
|
1450
|
+
// Let a deferred archive teardown finish before the record is revived, so
|
|
1451
|
+
// it can never close the revived agent.
|
|
1452
|
+
await this.awaitArchiveTeardown(agentId);
|
|
1388
1453
|
const registry = this.requireRegistry();
|
|
1389
1454
|
const record = await registry.get(agentId);
|
|
1390
1455
|
if (!record || !record.archivedAt) {
|
|
@@ -2046,8 +2111,7 @@ export class AgentManager {
|
|
|
2046
2111
|
};
|
|
2047
2112
|
}
|
|
2048
2113
|
async getLastAssistantMessageFromStores(agentId) {
|
|
2049
|
-
const
|
|
2050
|
-
const liveSegment = this.getLastAssistantMessageSegmentFromTimeline(liveTimeline);
|
|
2114
|
+
const liveSegment = this.timelineStore.getLastAssistantSegment(agentId);
|
|
2051
2115
|
if (!this.durableTimelineStore) {
|
|
2052
2116
|
return liveSegment?.text ?? null;
|
|
2053
2117
|
}
|
|
@@ -13,6 +13,8 @@ export declare class InMemoryAgentTimelineStore {
|
|
|
13
13
|
initialize(agentId: string, options?: SeedAgentTimelineOptions): void;
|
|
14
14
|
delete(agentId: string): void;
|
|
15
15
|
getItems(agentId: string): AgentTimelineItem[];
|
|
16
|
+
/** Row count without copying the timeline. */
|
|
17
|
+
size(agentId: string): number;
|
|
16
18
|
getRows(agentId: string): AgentTimelineRow[];
|
|
17
19
|
getEpoch(agentId: string): string;
|
|
18
20
|
fetch(agentId: string, options?: AgentTimelineFetchOptions): AgentTimelineFetchResult;
|
|
@@ -21,6 +23,14 @@ export declare class InMemoryAgentTimelineStore {
|
|
|
21
23
|
}): AgentTimelineRow;
|
|
22
24
|
getLastItem(agentId: string): AgentTimelineItem | null;
|
|
23
25
|
getLastAssistantMessage(agentId: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* The last contiguous run of assistant_message items (Claude streams chunks),
|
|
28
|
+
* walked from the end in place, and whether it starts at the first row.
|
|
29
|
+
*/
|
|
30
|
+
getLastAssistantSegment(agentId: string): {
|
|
31
|
+
text: string;
|
|
32
|
+
startsAtBeginning: boolean;
|
|
33
|
+
} | null;
|
|
24
34
|
private requireState;
|
|
25
35
|
private buildRowsFromItems;
|
|
26
36
|
}
|
|
@@ -16,13 +16,47 @@ const MAX_TIMELINE_FETCH_BYTES = 2 * 1024 * 1024;
|
|
|
16
16
|
function cloneRow(row) {
|
|
17
17
|
return { ...row };
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Serialized size per stored row, computed once. Rows are append-only and never
|
|
21
|
+
* edited in place, so the size of a row never changes; without this every page
|
|
22
|
+
* fetch re-serialized every row it considered (a tool result can be megabytes).
|
|
23
|
+
*/
|
|
24
|
+
const rowByteCache = new WeakMap();
|
|
19
25
|
function rowBytes(row) {
|
|
26
|
+
const cached = rowByteCache.get(row);
|
|
27
|
+
if (cached !== undefined) {
|
|
28
|
+
return cached;
|
|
29
|
+
}
|
|
30
|
+
let bytes = 0;
|
|
20
31
|
try {
|
|
21
|
-
|
|
32
|
+
bytes = JSON.stringify(row.item)?.length ?? 0;
|
|
22
33
|
}
|
|
23
34
|
catch {
|
|
24
|
-
|
|
35
|
+
bytes = 0;
|
|
25
36
|
}
|
|
37
|
+
rowByteCache.set(row, bytes);
|
|
38
|
+
return bytes;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Index of the first row whose seq is strictly greater than `seq` (or at least
|
|
42
|
+
* `seq` when `inclusive`), or `rows.length` when there is none. Rows are kept in
|
|
43
|
+
* ascending seq order, so this is a binary search instead of a linear scan of a
|
|
44
|
+
* timeline that can hold tens of thousands of rows.
|
|
45
|
+
*/
|
|
46
|
+
function firstIndexAfterSeq(rows, seq, inclusive = false) {
|
|
47
|
+
let low = 0;
|
|
48
|
+
let high = rows.length;
|
|
49
|
+
while (low < high) {
|
|
50
|
+
const mid = (low + high) >>> 1;
|
|
51
|
+
const rowSeq = rows[mid].seq;
|
|
52
|
+
if (inclusive ? rowSeq >= seq : rowSeq > seq) {
|
|
53
|
+
high = mid;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
low = mid + 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return low;
|
|
26
60
|
}
|
|
27
61
|
/**
|
|
28
62
|
* Trim a selected page to {@link MAX_TIMELINE_FETCH_BYTES}, dropping rows from
|
|
@@ -72,8 +106,8 @@ function fetchTail(ctx) {
|
|
|
72
106
|
function fetchAfter(ctx) {
|
|
73
107
|
const { state, direction, limit, selectAll, cursor, minSeq, maxSeq, window } = ctx;
|
|
74
108
|
const baseSeq = cursor?.seq ?? 0;
|
|
75
|
-
const startIdx = state.rows
|
|
76
|
-
if (startIdx
|
|
109
|
+
const startIdx = firstIndexAfterSeq(state.rows, baseSeq);
|
|
110
|
+
if (startIdx >= state.rows.length) {
|
|
77
111
|
return {
|
|
78
112
|
epoch: state.epoch,
|
|
79
113
|
direction,
|
|
@@ -103,7 +137,8 @@ function fetchAfter(ctx) {
|
|
|
103
137
|
function fetchBefore(ctx) {
|
|
104
138
|
const { state, direction, limit, selectAll, cursor, minSeq, window } = ctx;
|
|
105
139
|
const beforeSeq = cursor?.seq ?? state.nextSeq;
|
|
106
|
-
const
|
|
140
|
+
const firstAtOrAfter = firstIndexAfterSeq(state.rows, beforeSeq, true);
|
|
141
|
+
const endExclusive = firstAtOrAfter >= state.rows.length ? -1 : firstAtOrAfter;
|
|
107
142
|
const boundedRows = endExclusive < 0 ? state.rows : state.rows.slice(0, endExclusive);
|
|
108
143
|
const selected = applyByteBudget(selectAll || limit >= boundedRows.length
|
|
109
144
|
? boundedRows
|
|
@@ -162,6 +197,10 @@ export class InMemoryAgentTimelineStore {
|
|
|
162
197
|
getItems(agentId) {
|
|
163
198
|
return this.requireState(agentId).rows.map((row) => row.item);
|
|
164
199
|
}
|
|
200
|
+
/** Row count without copying the timeline. */
|
|
201
|
+
size(agentId) {
|
|
202
|
+
return this.requireState(agentId).rows.length;
|
|
203
|
+
}
|
|
165
204
|
getRows(agentId) {
|
|
166
205
|
return this.requireState(agentId).rows.map(cloneRow);
|
|
167
206
|
}
|
|
@@ -237,8 +276,16 @@ export class InMemoryAgentTimelineStore {
|
|
|
237
276
|
return state.rows[state.rows.length - 1]?.item ?? null;
|
|
238
277
|
}
|
|
239
278
|
getLastAssistantMessage(agentId) {
|
|
279
|
+
return this.getLastAssistantSegment(agentId)?.text ?? null;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The last contiguous run of assistant_message items (Claude streams chunks),
|
|
283
|
+
* walked from the end in place, and whether it starts at the first row.
|
|
284
|
+
*/
|
|
285
|
+
getLastAssistantSegment(agentId) {
|
|
240
286
|
const rows = this.requireState(agentId).rows;
|
|
241
287
|
const chunks = [];
|
|
288
|
+
let startsAtBeginning = false;
|
|
242
289
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
243
290
|
const item = rows[i].item;
|
|
244
291
|
if (item.type !== "assistant_message") {
|
|
@@ -248,11 +295,12 @@ export class InMemoryAgentTimelineStore {
|
|
|
248
295
|
continue;
|
|
249
296
|
}
|
|
250
297
|
chunks.push(item.text);
|
|
298
|
+
startsAtBeginning = i === 0;
|
|
251
299
|
}
|
|
252
300
|
if (chunks.length === 0) {
|
|
253
301
|
return null;
|
|
254
302
|
}
|
|
255
|
-
return chunks.toReversed().join("");
|
|
303
|
+
return { text: chunks.toReversed().join(""), startsAtBeginning };
|
|
256
304
|
}
|
|
257
305
|
requireState(agentId) {
|
|
258
306
|
const state = this.states.get(agentId);
|
|
@@ -50,6 +50,8 @@ export declare class BlockerLog {
|
|
|
50
50
|
private readonly logger;
|
|
51
51
|
private readonly now;
|
|
52
52
|
private ensured;
|
|
53
|
+
/** Per-card view of the ledger, kept current incrementally (see JsonlCardIndex). */
|
|
54
|
+
private readonly index;
|
|
53
55
|
constructor(options: BlockerLogOptions);
|
|
54
56
|
/**
|
|
55
57
|
* Record the current blocker set for a card. No-ops when the set is unchanged
|
|
@@ -60,7 +62,7 @@ export declare class BlockerLog {
|
|
|
60
62
|
record(cardId: string, blockers: readonly string[] | undefined): void;
|
|
61
63
|
/** All rows, oldest first. Returns [] when the log does not exist yet. */
|
|
62
64
|
readAll(): BlockerRow[];
|
|
63
|
-
/**
|
|
65
|
+
/** The newest rows for one card (at most DEFAULT_ROWS_PER_CARD), oldest first. */
|
|
64
66
|
history(cardId: string): BlockerRow[];
|
|
65
67
|
/** The most recent recording for a card, or null. */
|
|
66
68
|
latest(cardId: string): BlockerRow | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { JsonlCardIndex } from "./jsonl-card-index.js";
|
|
3
4
|
/** Change detection only. NOT used for matching or counting. */
|
|
4
5
|
function fingerprint(blockers) {
|
|
5
6
|
return blockers
|
|
@@ -14,6 +15,18 @@ export class BlockerLog {
|
|
|
14
15
|
this.filePath = path.join(options.dir, "blockers.jsonl");
|
|
15
16
|
this.logger = options.logger;
|
|
16
17
|
this.now = options.now ?? (() => new Date());
|
|
18
|
+
this.index = new JsonlCardIndex({
|
|
19
|
+
filePath: this.filePath,
|
|
20
|
+
parse: (line) => {
|
|
21
|
+
const parsed = JSON.parse(line);
|
|
22
|
+
return typeof parsed?.cardId === "string" && Array.isArray(parsed?.blockers)
|
|
23
|
+
? parsed
|
|
24
|
+
: null;
|
|
25
|
+
},
|
|
26
|
+
cardIdOf: (row) => row.cardId,
|
|
27
|
+
count: (row) => ({ blocked: row.blockers.length > 0 ? 1 : 0 }),
|
|
28
|
+
onReadError: (error) => this.logger.error({ err: error }, "Blocker log read failed"),
|
|
29
|
+
});
|
|
17
30
|
}
|
|
18
31
|
/**
|
|
19
32
|
* Record the current blocker set for a card. No-ops when the set is unchanged
|
|
@@ -73,14 +86,13 @@ export class BlockerLog {
|
|
|
73
86
|
}
|
|
74
87
|
return rows;
|
|
75
88
|
}
|
|
76
|
-
/**
|
|
89
|
+
/** The newest rows for one card (at most DEFAULT_ROWS_PER_CARD), oldest first. */
|
|
77
90
|
history(cardId) {
|
|
78
|
-
return this.
|
|
91
|
+
return [...(this.index.get(cardId)?.rows ?? [])];
|
|
79
92
|
}
|
|
80
93
|
/** The most recent recording for a card, or null. */
|
|
81
94
|
latest(cardId) {
|
|
82
|
-
|
|
83
|
-
return rows.length > 0 ? rows[rows.length - 1] : null;
|
|
95
|
+
return this.index.get(cardId)?.rows.at(-1) ?? null;
|
|
84
96
|
}
|
|
85
97
|
/**
|
|
86
98
|
* How many separate times this card has been recorded as blocked by anything.
|
|
@@ -88,7 +100,7 @@ export class BlockerLog {
|
|
|
88
100
|
* Marco specifically", because that is a semantic question, see the module note.
|
|
89
101
|
*/
|
|
90
102
|
blockedRecordings(cardId) {
|
|
91
|
-
return this.
|
|
103
|
+
return this.index.get(cardId)?.counters.blocked ?? 0;
|
|
92
104
|
}
|
|
93
105
|
/**
|
|
94
106
|
* Compact history for a judge briefing: one line per distinct recording,
|
|
@@ -36,6 +36,8 @@ export declare class CardMoveLog {
|
|
|
36
36
|
private readonly logger;
|
|
37
37
|
private readonly now;
|
|
38
38
|
private ensured;
|
|
39
|
+
/** Per-card view of the ledger, kept current incrementally (see JsonlCardIndex). */
|
|
40
|
+
private readonly index;
|
|
39
41
|
constructor(options: CardMoveLogOptions);
|
|
40
42
|
/**
|
|
41
43
|
* Record a column change. No-ops when the column did not actually change, so
|
|
@@ -46,7 +48,7 @@ export declare class CardMoveLog {
|
|
|
46
48
|
record(cardId: string, fromColumn: string | null, toColumn: string | null): void;
|
|
47
49
|
/** All rows, oldest first. Returns [] when the log does not exist yet. */
|
|
48
50
|
readAll(): CardMoveRow[];
|
|
49
|
-
/**
|
|
51
|
+
/** The newest rows for one card (at most DEFAULT_ROWS_PER_CARD), oldest first. */
|
|
50
52
|
history(cardId: string): CardMoveRow[];
|
|
51
53
|
/**
|
|
52
54
|
* How many times this card has been pushed away from active work.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { JsonlCardIndex } from "./jsonl-card-index.js";
|
|
3
4
|
/** Columns that mean "pushed away from now", used for the deferral count. */
|
|
4
5
|
const DEFERRAL_TARGETS = new Set(["later", "future", "awaiting"]);
|
|
5
6
|
/** Columns that mean "actively committed to". */
|
|
@@ -10,6 +11,16 @@ export class CardMoveLog {
|
|
|
10
11
|
this.filePath = path.join(options.dir, "card-moves.jsonl");
|
|
11
12
|
this.logger = options.logger;
|
|
12
13
|
this.now = options.now ?? (() => new Date());
|
|
14
|
+
this.index = new JsonlCardIndex({
|
|
15
|
+
filePath: this.filePath,
|
|
16
|
+
parse: (line) => {
|
|
17
|
+
const parsed = JSON.parse(line);
|
|
18
|
+
return typeof parsed?.cardId === "string" && typeof parsed?.at === "string" ? parsed : null;
|
|
19
|
+
},
|
|
20
|
+
cardIdOf: (row) => row.cardId,
|
|
21
|
+
count: (row) => ({ deferrals: isDeferral(row) ? 1 : 0 }),
|
|
22
|
+
onReadError: (error) => this.logger.error({ err: error }, "Card move log read failed"),
|
|
23
|
+
});
|
|
13
24
|
}
|
|
14
25
|
/**
|
|
15
26
|
* Record a column change. No-ops when the column did not actually change, so
|
|
@@ -71,9 +82,9 @@ export class CardMoveLog {
|
|
|
71
82
|
}
|
|
72
83
|
return rows;
|
|
73
84
|
}
|
|
74
|
-
/**
|
|
85
|
+
/** The newest rows for one card (at most DEFAULT_ROWS_PER_CARD), oldest first. */
|
|
75
86
|
history(cardId) {
|
|
76
|
-
return this.
|
|
87
|
+
return [...(this.index.get(cardId)?.rows ?? [])];
|
|
77
88
|
}
|
|
78
89
|
/**
|
|
79
90
|
* How many times this card has been pushed away from active work.
|
|
@@ -85,21 +96,11 @@ export class CardMoveLog {
|
|
|
85
96
|
* a lie.
|
|
86
97
|
*/
|
|
87
98
|
deferralCount(cardId) {
|
|
88
|
-
|
|
89
|
-
for (const row of this.history(cardId)) {
|
|
90
|
-
if (row.fromColumn &&
|
|
91
|
-
ACTIVE_SOURCES.has(row.fromColumn) &&
|
|
92
|
-
row.toColumn &&
|
|
93
|
-
DEFERRAL_TARGETS.has(row.toColumn)) {
|
|
94
|
-
count += 1;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return count;
|
|
99
|
+
return this.index.get(cardId)?.counters.deferrals ?? 0;
|
|
98
100
|
}
|
|
99
101
|
/** When the card last entered its current column, from the log. Null if unknown. */
|
|
100
102
|
lastMoveAt(cardId) {
|
|
101
|
-
|
|
102
|
-
return history.length > 0 ? history[history.length - 1].at : null;
|
|
103
|
+
return this.index.get(cardId)?.rows.at(-1)?.at ?? null;
|
|
103
104
|
}
|
|
104
105
|
ensureDir() {
|
|
105
106
|
if (this.ensured) {
|
|
@@ -109,5 +110,11 @@ export class CardMoveLog {
|
|
|
109
110
|
this.ensured = true;
|
|
110
111
|
}
|
|
111
112
|
}
|
|
113
|
+
function isDeferral(row) {
|
|
114
|
+
return Boolean(row.fromColumn &&
|
|
115
|
+
ACTIVE_SOURCES.has(row.fromColumn) &&
|
|
116
|
+
row.toColumn &&
|
|
117
|
+
DEFERRAL_TARGETS.has(row.toColumn));
|
|
118
|
+
}
|
|
112
119
|
export const KANBAN_LABEL_KEY = "kanban";
|
|
113
120
|
//# sourceMappingURL=card-move-log.js.map
|
|
@@ -88,7 +88,7 @@ export async function importProviderSession(input) {
|
|
|
88
88
|
await unarchiveAgentState(input.agentStorage, input.agentManager, snapshot.id);
|
|
89
89
|
return {
|
|
90
90
|
snapshot,
|
|
91
|
-
timelineSize: input.agentManager.
|
|
91
|
+
timelineSize: input.agentManager.getTimelineSize(snapshot.id),
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
async function unarchiveAgentByHandle(agentStorage, agentManager, handle) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-card index over an append-only `*.jsonl` ledger (blockers.jsonl,
|
|
3
|
+
* card-moves.jsonl), so per-card reads stop re-reading and re-parsing the whole
|
|
4
|
+
* file on every digest write.
|
|
5
|
+
*
|
|
6
|
+
* - Incremental: each access `stat`s the file and parses only the bytes appended
|
|
7
|
+
* since the last read, so rows appended by another process (a loop script) are
|
|
8
|
+
* seen on the next access. A file that shrank (rotated or rewritten) is
|
|
9
|
+
* re-indexed from scratch.
|
|
10
|
+
* - Bounded: a card keeps its newest `rowsPerCard` rows plus running counters
|
|
11
|
+
* supplied by the caller (`count`), and at most `maxCards` cards are held,
|
|
12
|
+
* least recently used evicted first. A read for an evicted card rebuilds that
|
|
13
|
+
* one card with a full scan, which is the old cost, paid only on a miss.
|
|
14
|
+
*/
|
|
15
|
+
export interface JsonlCardIndexOptions<Row> {
|
|
16
|
+
filePath: string;
|
|
17
|
+
parse(line: string): Row | null;
|
|
18
|
+
cardIdOf(row: Row): string;
|
|
19
|
+
/** Running counters kept per card across the whole history, e.g. deferrals. */
|
|
20
|
+
count?: (row: Row) => Record<string, number>;
|
|
21
|
+
rowsPerCard?: number;
|
|
22
|
+
maxCards?: number;
|
|
23
|
+
onReadError?: (error: unknown) => void;
|
|
24
|
+
}
|
|
25
|
+
export interface CardEntry<Row> {
|
|
26
|
+
/** Newest rows for the card, oldest first, at most `rowsPerCard`. */
|
|
27
|
+
rows: Row[];
|
|
28
|
+
/** Every row ever indexed for the card, including trimmed ones. */
|
|
29
|
+
total: number;
|
|
30
|
+
counters: Record<string, number>;
|
|
31
|
+
}
|
|
32
|
+
export declare const DEFAULT_ROWS_PER_CARD = 64;
|
|
33
|
+
export declare const DEFAULT_MAX_CARDS = 4096;
|
|
34
|
+
export declare class JsonlCardIndex<Row> {
|
|
35
|
+
private readonly options;
|
|
36
|
+
private readonly cards;
|
|
37
|
+
private offset;
|
|
38
|
+
private mtimeMs;
|
|
39
|
+
private partial;
|
|
40
|
+
private evicted;
|
|
41
|
+
private readonly rowsPerCard;
|
|
42
|
+
private readonly maxCards;
|
|
43
|
+
constructor(options: JsonlCardIndexOptions<Row>);
|
|
44
|
+
/** The card's entry, current with the file. */
|
|
45
|
+
get(cardId: string): CardEntry<Row> | null;
|
|
46
|
+
/** Read whatever was appended since the last call. */
|
|
47
|
+
sync(): void;
|
|
48
|
+
private parseLine;
|
|
49
|
+
private add;
|
|
50
|
+
private rebuildCard;
|
|
51
|
+
private reset;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=jsonl-card-index.d.ts.map
|