@50arms/session-telemetry 0.0.0-stage → 0.1.66
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/README.md +68 -2
- package/dist/.source-hash +1 -0
- package/dist/activity-observer.d.ts +186 -0
- package/dist/activity-observer.js +917 -0
- package/dist/api-tool-names.d.ts +12 -0
- package/dist/api-tool-names.js +19 -0
- package/dist/browser.d.ts +29 -0
- package/dist/browser.js +10 -0
- package/dist/catalog-prices.d.ts +27 -0
- package/dist/catalog-prices.js +31 -0
- package/dist/claim-tracker.d.ts +53 -0
- package/dist/claim-tracker.js +213 -0
- package/dist/dispatch-usage.d.ts +321 -0
- package/dist/dispatch-usage.js +657 -0
- package/dist/git-head.d.ts +1 -0
- package/dist/git-head.js +19 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +49 -0
- package/dist/internal-logger.d.ts +16 -0
- package/dist/internal-logger.js +27 -0
- package/dist/ledger-timing.d.ts +26 -0
- package/dist/ledger-timing.js +31 -0
- package/dist/pricing.d.ts +93 -0
- package/dist/pricing.js +77 -0
- package/dist/real-work.d.ts +13 -0
- package/dist/real-work.js +57 -0
- package/dist/session-log-watcher.d.ts +616 -0
- package/dist/session-log-watcher.js +1285 -0
- package/dist/session-title-marks.d.ts +2 -0
- package/dist/session-title-marks.js +16 -0
- package/dist/stats-wire.d.ts +345 -0
- package/dist/stats-wire.js +636 -0
- package/dist/subagent-live.d.ts +36 -0
- package/dist/subagent-live.js +153 -0
- package/dist/tool-summary.d.ts +19 -0
- package/dist/tool-summary.js +58 -0
- package/dist/transcript-timing.d.ts +100 -0
- package/dist/transcript-timing.js +231 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.js +1 -0
- package/dist/usage-ledger.d.ts +256 -0
- package/dist/usage-ledger.js +581 -0
- package/dist/write-queue.d.ts +9 -0
- package/dist/write-queue.js +16 -0
- package/package.json +43 -4
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names a dashboard API tool call can carry in a transcript: a leaf module, because `claim-tracker.ts` and `real-work.ts` both read it and each is
|
|
3
|
+
* reached from the other through `usage-ledger.ts`.
|
|
4
|
+
*/
|
|
5
|
+
/** The server key a dispatched worker's clean-room config names, and the 50arms plugin's, which an operator session's sub-agent calls: the current prefixes. */
|
|
6
|
+
export declare const CURRENT_API_SERVER_PREFIXES: readonly string[];
|
|
7
|
+
/** The prefixes of the server keys this product no longer uses, still held by older sessions and by stored transcripts. DX-6864 removes them. */
|
|
8
|
+
export declare const RETIRED_API_SERVER_PREFIXES: readonly string[];
|
|
9
|
+
/** Every server prefix a dashboard API tool name can start with. */
|
|
10
|
+
export declare const API_SERVER_PREFIXES: readonly string[];
|
|
11
|
+
/** The generic API tool, by its current name and by the name every release serves until DX-6864 removes it. */
|
|
12
|
+
export declare const API_TOOL_BASE_NAMES: readonly string[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names a dashboard API tool call can carry in a transcript: a leaf module, because `claim-tracker.ts` and `real-work.ts` both read it and each is
|
|
3
|
+
* reached from the other through `usage-ledger.ts`.
|
|
4
|
+
*/
|
|
5
|
+
// DX-4578: these names are live, none a fallback. A dispatched worker's clean-room config names the server `platform` (DX-6847; it was `danx-dashboard`); an
|
|
6
|
+
// operator session's sub-agent reaches it through the 50arms plugin's server (`plugin:50arms:platform`, tools `mcp__plugin_50arms_platform__*`).
|
|
7
|
+
// DX-6847 / DX-6864 (DEC-571): the server key and the tool name each changed, and a transcript is recognised by what its session called, so every pair a
|
|
8
|
+
// session or a stored transcript can hold is credited until DX-6864 removes the old tool name and the old keys have left every machine:
|
|
9
|
+
// - the server key `danx-dashboard` of a dispatched worker before DX-6847, the plugin's `plugin_50arms_dashboard` before CP-70, and the pre-DX-6845
|
|
10
|
+
// plugin's `plugin_danxbot_danx-dashboard` (a session started before either rename still runs that plugin and its tools keep that prefix);
|
|
11
|
+
// - the tool `danxbot_api`, which every package release up to DX-6864 serves beside `50arms_api`.
|
|
12
|
+
/** The server key a dispatched worker's clean-room config names, and the 50arms plugin's, which an operator session's sub-agent calls: the current prefixes. */
|
|
13
|
+
export const CURRENT_API_SERVER_PREFIXES = ["mcp__platform__", "mcp__plugin_50arms_platform__"];
|
|
14
|
+
/** The prefixes of the server keys this product no longer uses, still held by older sessions and by stored transcripts. DX-6864 removes them. */
|
|
15
|
+
export const RETIRED_API_SERVER_PREFIXES = ["mcp__danx-dashboard__", "mcp__plugin_50arms_dashboard__", "mcp__plugin_danxbot_danx-dashboard__"];
|
|
16
|
+
/** Every server prefix a dashboard API tool name can start with. */
|
|
17
|
+
export const API_SERVER_PREFIXES = [...CURRENT_API_SERVER_PREFIXES, ...RETIRED_API_SERVER_PREFIXES];
|
|
18
|
+
/** The generic API tool, by its current name and by the name every release serves until DX-6864 removes it. */
|
|
19
|
+
export const API_TOOL_BASE_NAMES = ["50arms_api", "danxbot_api"];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2962 — browser-safe subpath (`@50arms/session-telemetry/browser`).
|
|
3
|
+
*
|
|
4
|
+
* This file MUST NEVER import a `node:` builtin, directly or transitively —
|
|
5
|
+
* enforced by `src/__tests__/no-node-imports.test.ts`, which greps every
|
|
6
|
+
* file this module reaches. `session-log-watcher.ts` (the main entry's
|
|
7
|
+
* collector) imports `node:fs`/`node:fs/promises`/`node:os`/`node:path`, so
|
|
8
|
+
* it — and `internal-logger.ts` and `tool-summary.ts`, which it alone pulls
|
|
9
|
+
* in — are deliberately NOT re-exported here.
|
|
10
|
+
*
|
|
11
|
+
* Only the two type-only exports are browser-safe today: `AgentLogEntry` /
|
|
12
|
+
* `AgentLineage` carry no runtime code at all, so any consumer that needs
|
|
13
|
+
* to describe a transcript entry's SHAPE (the dashboard server, the
|
|
14
|
+
* dashboard SPA, a future browser-side usage display) can depend on this
|
|
15
|
+
* subpath with zero risk of pulling in a Node builtin.
|
|
16
|
+
*
|
|
17
|
+
* DX-2963 adds the first real runtime exports here: the one usage counter
|
|
18
|
+
* (`usage-ledger.ts`) and the one pricing function (`pricing.ts`), both pure
|
|
19
|
+
* arithmetic with no I/O — `usage-snapshot.ts` in danxbot root (bundled by the
|
|
20
|
+
* React frontend) imports through THIS subpath, never the main entry.
|
|
21
|
+
*/
|
|
22
|
+
export type { AgentLogEntry, AgentLineage } from "./types.js";
|
|
23
|
+
export { truncStr } from "./tool-summary.js";
|
|
24
|
+
export * from "./usage-ledger.js";
|
|
25
|
+
export * from "./real-work.js";
|
|
26
|
+
export * from "./ledger-timing.js";
|
|
27
|
+
export * from "./transcript-timing.js";
|
|
28
|
+
export * from "./pricing.js";
|
|
29
|
+
export * from "./catalog-prices.js";
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// `truncStr` is pure string arithmetic (no `node:` imports, no I/O) — safe
|
|
2
|
+
// to carry on the browser subpath alongside the two types above.
|
|
3
|
+
export { truncStr } from "./tool-summary.js";
|
|
4
|
+
// DX-2963 — counter + pricing (pure arithmetic, no `node:` imports).
|
|
5
|
+
export * from "./usage-ledger.js";
|
|
6
|
+
export * from "./real-work.js";
|
|
7
|
+
export * from "./ledger-timing.js";
|
|
8
|
+
export * from "./transcript-timing.js";
|
|
9
|
+
export * from "./pricing.js";
|
|
10
|
+
export * from "./catalog-prices.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-5611 (PLAN-53 G-3) - the ONE builder of the price lookups every pricing function takes, from the model-version catalog's rows
|
|
3
|
+
* (`model_versions` / `model_version_prices`, as the dashboard answers them). The danxbot server, the bench worker and the MCP's
|
|
4
|
+
* `subagents-live` all hold the catalog in this shape, so none spells the price fields or the long-prompt pairing again. Pure, no `node:`
|
|
5
|
+
* imports.
|
|
6
|
+
*/
|
|
7
|
+
import type { PriceCardLookup, PriceLookup } from "./pricing.js";
|
|
8
|
+
/** One `model_version_prices` row: USD per million tokens; `long_prompt_above_tokens` is set exactly on a `long_prompt` row. */
|
|
9
|
+
export interface CatalogPriceRow {
|
|
10
|
+
speed: string;
|
|
11
|
+
band: string;
|
|
12
|
+
input: number;
|
|
13
|
+
output: number;
|
|
14
|
+
cache_write_5m: number;
|
|
15
|
+
cache_write_1h: number;
|
|
16
|
+
cache_read: number;
|
|
17
|
+
long_prompt_above_tokens?: number | null;
|
|
18
|
+
}
|
|
19
|
+
/** The part of a declared model version pricing needs. */
|
|
20
|
+
export interface CatalogVersionPrices {
|
|
21
|
+
model_id: string;
|
|
22
|
+
prices: readonly CatalogPriceRow[];
|
|
23
|
+
}
|
|
24
|
+
/** The rate cards of every declared version: a speed's `standard` band row is its card, its `long_prompt` band row the second card. */
|
|
25
|
+
export declare function priceCardsOfCatalog(versions: readonly CatalogVersionPrices[]): PriceCardLookup;
|
|
26
|
+
/** The standard-band price of a model at a speed: what a usage bucket (many replies) is priced with. */
|
|
27
|
+
export declare function priceLookupOfCards(cardsOf: PriceCardLookup): PriceLookup;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const modelPriceOfRow = (p) => ({
|
|
2
|
+
input: p.input,
|
|
3
|
+
output: p.output,
|
|
4
|
+
cacheWrite5m: p.cache_write_5m,
|
|
5
|
+
cacheWrite1h: p.cache_write_1h,
|
|
6
|
+
cacheRead: p.cache_read,
|
|
7
|
+
});
|
|
8
|
+
const key = (modelId, speed) => JSON.stringify([modelId, speed]);
|
|
9
|
+
/** The rate cards of every declared version: a speed's `standard` band row is its card, its `long_prompt` band row the second card. */
|
|
10
|
+
export function priceCardsOfCatalog(versions) {
|
|
11
|
+
const cards = new Map();
|
|
12
|
+
for (const v of versions) {
|
|
13
|
+
for (const row of v.prices) {
|
|
14
|
+
if (row.band !== "standard")
|
|
15
|
+
continue;
|
|
16
|
+
const long = v.prices.find((p) => p.speed === row.speed && p.band === "long_prompt");
|
|
17
|
+
let longPrompt = null;
|
|
18
|
+
if (long !== undefined) {
|
|
19
|
+
if (long.long_prompt_above_tokens == null)
|
|
20
|
+
throw new Error(`model "${v.model_id}" ${long.speed} long_prompt price row has no long_prompt_above_tokens`);
|
|
21
|
+
longPrompt = { aboveTokens: long.long_prompt_above_tokens, price: modelPriceOfRow(long) };
|
|
22
|
+
}
|
|
23
|
+
cards.set(key(v.model_id, row.speed), { standard: modelPriceOfRow(row), longPrompt });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return (model, speed) => cards.get(key(model, speed)) ?? null;
|
|
27
|
+
}
|
|
28
|
+
/** The standard-band price of a model at a speed: what a usage bucket (many replies) is priced with. */
|
|
29
|
+
export function priceLookupOfCards(cardsOf) {
|
|
30
|
+
return (model, speed) => cardsOf(model, speed)?.standard ?? null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The `50arms_api` tool of a dispatched worker's `platform` server. */
|
|
2
|
+
export declare const DISPATCH_API_TOOL_NAME = "mcp__platform__50arms_api";
|
|
3
|
+
/** Every tool name a sub-agent reaches the dashboard API through. */
|
|
4
|
+
export declare const DASHBOARD_API_TOOL_NAMES: ReadonlySet<string>;
|
|
5
|
+
/**
|
|
6
|
+
* A card id as the issue routes name it, e.g. `DX-3962`, or an imported card's `DX-3962-IMPORT`, `DX-3962-IMPORT-2`,
|
|
7
|
+
* `DX-3962-IMPORTED` (DX-6042: the danxbot card-id grammar in src/issues/card-id.ts). Also the wire's validation for
|
|
8
|
+
* every card id a claim carries.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ISSUE_ID_PATTERN: RegExp;
|
|
11
|
+
/** Longest card id a claim may carry (`dispatches.issue_id` is `VARCHAR(32)`). */
|
|
12
|
+
export declare const ISSUE_ID_MAX_LENGTH = 32;
|
|
13
|
+
/**
|
|
14
|
+
* Bounds that keep one source's persisted state, and the stats write that carries
|
|
15
|
+
* it, bounded. A real sub-agent holds one or two cards, so neither is reachable;
|
|
16
|
+
* the OLDEST entry is the one dropped, because the newest claim is what the next
|
|
17
|
+
* reply is credited to.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_HELD_CLAIMS = 50;
|
|
20
|
+
export declare const MAX_PENDING_TRANSITIONS = 50;
|
|
21
|
+
/** One transition whose `tool_use` has been read and whose `tool_result` has not. */
|
|
22
|
+
export interface PendingTransition {
|
|
23
|
+
op: "claim" | "release";
|
|
24
|
+
issueId: string;
|
|
25
|
+
}
|
|
26
|
+
/** One source's claim state at one transcript position. */
|
|
27
|
+
export interface ClaimState {
|
|
28
|
+
/** Claimed cards, oldest first. */
|
|
29
|
+
held: string[];
|
|
30
|
+
/** In-flight transitions keyed by their `tool_use_id`. */
|
|
31
|
+
pending: Record<string, PendingTransition>;
|
|
32
|
+
}
|
|
33
|
+
export declare function emptyClaimState(): ClaimState;
|
|
34
|
+
/**
|
|
35
|
+
* The per-process claim tracker: one state per sub-agent source. Fed every
|
|
36
|
+
* transcript line by `DispatchUsage` (`operator-bridge` mode only — the worker never
|
|
37
|
+
* constructs one), seeded from the stored cursor at attach, and snapshotted per
|
|
38
|
+
* source for the stats write.
|
|
39
|
+
*/
|
|
40
|
+
export declare class ClaimTracker {
|
|
41
|
+
private readonly states;
|
|
42
|
+
/** Replace a source's state — at attach (the stored cursor's claims) and on a past-EOF reset (empty). */
|
|
43
|
+
seed(source: string, state: ClaimState): void;
|
|
44
|
+
/** A copy of the source's state as of the last line fed; empty for a source that holds nothing. */
|
|
45
|
+
snapshot(source: string): ClaimState;
|
|
46
|
+
/** The card a reply whose first line is read NOW is credited to; `null` when the source holds none. */
|
|
47
|
+
issueFor(source: string): string | null;
|
|
48
|
+
/** An assistant line's content blocks: remember every claim / release the line asks for until its result arrives. */
|
|
49
|
+
observeAssistantBlocks(source: string, blocks: readonly unknown[]): void;
|
|
50
|
+
/** A user line's content blocks: settle every remembered transition whose result is here. */
|
|
51
|
+
observeResultBlocks(source: string, blocks: readonly unknown[]): void;
|
|
52
|
+
private stateOf;
|
|
53
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-3962 — which card a sub-agent is working, read from the sub-agent's OWN
|
|
3
|
+
* transcript.
|
|
4
|
+
*
|
|
5
|
+
* An operator session builds most cards through sub-agents that claim the card
|
|
6
|
+
* with a manual `pickup`, which creates no dispatch row. Every such claim is in
|
|
7
|
+
* the sub-agent's transcript: a `50arms_api` `tool_use`
|
|
8
|
+
* `POST /api/issues/<ID>/transition {action: "pickup"}` and, later, the
|
|
9
|
+
* `tool_result` carrying the server's answer. This tracker turns those into the
|
|
10
|
+
* card id every one of the sub-agent's replies is credited to.
|
|
11
|
+
*
|
|
12
|
+
* ## The rule (DX-3962 plan-architecture finding 5)
|
|
13
|
+
*
|
|
14
|
+
* Per source, an ORDERED set of claimed cards.
|
|
15
|
+
* - a successful `pickup` appends the card, or moves it to the end when it is
|
|
16
|
+
* already held;
|
|
17
|
+
* - a successful `complete` / `cancel` / `block` / `rollback_pickup` of a held
|
|
18
|
+
* card BY THE SAME SOURCE removes it. (`review` is not a transition action —
|
|
19
|
+
* Review is a derived status, `issues/derive/status.ts` — so the card's list
|
|
20
|
+
* of releases names only the four that exist.)
|
|
21
|
+
* - a reply's card is the LAST element of the set at the reply's first line, by
|
|
22
|
+
* transcript order, never wall clock. An empty set is `null`.
|
|
23
|
+
*
|
|
24
|
+
* Success is read from the RESULT, never the request: a claim or release takes
|
|
25
|
+
* effect only on the `tool_result` for its own `tool_use_id` whose body is
|
|
26
|
+
* `{"ok": true, ...}`. A refused pickup (409) never attributes and a refused
|
|
27
|
+
* release never releases.
|
|
28
|
+
*
|
|
29
|
+
* `main` never attributes (epic decision): its replies roll up to the plan or the
|
|
30
|
+
* board. Quality-gate verdicts, comments and edits are not transitions, so they
|
|
31
|
+
* never set a claim — a reviewer touching several cards stays unattributed.
|
|
32
|
+
* Documented unattributed cases: a nested sub-agent with no pickup of its own, and
|
|
33
|
+
* the reply that issued the pickup call itself (its first line precedes the
|
|
34
|
+
* successful result).
|
|
35
|
+
*
|
|
36
|
+
* ## The state belongs to a transcript position
|
|
37
|
+
*
|
|
38
|
+
* The state is the held set PLUS the transitions whose `tool_use` has been read
|
|
39
|
+
* and whose `tool_result` has not (`pending`). Both are persisted beside the
|
|
40
|
+
* source's read cursor (`dispatch_source_cursors.claims`): a relay that restarts
|
|
41
|
+
* resumes at its stored `acked` offset, and a pickup that sits before it is never
|
|
42
|
+
* re-read, so without the state every later reply would go unattributed. The
|
|
43
|
+
* pending half closes the narrow window where the restart lands between a pickup's
|
|
44
|
+
* `tool_use` and its `tool_result`.
|
|
45
|
+
*
|
|
46
|
+
* Pure bookkeeping — no `node:` imports, so it rides the browser-safe subpath too.
|
|
47
|
+
*/
|
|
48
|
+
import { API_SERVER_PREFIXES, API_TOOL_BASE_NAMES } from "./api-tool-names.js";
|
|
49
|
+
import { MAIN_SOURCE } from "./usage-ledger.js";
|
|
50
|
+
/** The `50arms_api` tool of a dispatched worker's `platform` server. */
|
|
51
|
+
export const DISPATCH_API_TOOL_NAME = "mcp__platform__50arms_api";
|
|
52
|
+
/** Every tool name a sub-agent reaches the dashboard API through. */
|
|
53
|
+
export const DASHBOARD_API_TOOL_NAMES = new Set(API_SERVER_PREFIXES.flatMap((prefix) => API_TOOL_BASE_NAMES.map((tool) => prefix + tool)));
|
|
54
|
+
/**
|
|
55
|
+
* A card id as the issue routes name it, e.g. `DX-3962`, or an imported card's `DX-3962-IMPORT`, `DX-3962-IMPORT-2`,
|
|
56
|
+
* `DX-3962-IMPORTED` (DX-6042: the danxbot card-id grammar in src/issues/card-id.ts). Also the wire's validation for
|
|
57
|
+
* every card id a claim carries.
|
|
58
|
+
*/
|
|
59
|
+
export const ISSUE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*-[0-9]+(?:-IMPORT(?:ED)?(?:-[0-9]+)?)?$/;
|
|
60
|
+
/** Longest card id a claim may carry (`dispatches.issue_id` is `VARCHAR(32)`). */
|
|
61
|
+
export const ISSUE_ID_MAX_LENGTH = 32;
|
|
62
|
+
/** The transition that claims a card. */
|
|
63
|
+
const CLAIM_ACTION = "pickup";
|
|
64
|
+
/** The transitions that end a sub-agent's hold on a card. */
|
|
65
|
+
const RELEASE_ACTIONS = new Set(["complete", "cancel", "block", "rollback_pickup"]);
|
|
66
|
+
// DX-3962: built from ISSUE_ID_PATTERN so the card-id shape lives in one place.
|
|
67
|
+
const TRANSITION_PATH = new RegExp(`^/api/issues/(${ISSUE_ID_PATTERN.source.slice(1, -1)})/transition$`);
|
|
68
|
+
/**
|
|
69
|
+
* Bounds that keep one source's persisted state, and the stats write that carries
|
|
70
|
+
* it, bounded. A real sub-agent holds one or two cards, so neither is reachable;
|
|
71
|
+
* the OLDEST entry is the one dropped, because the newest claim is what the next
|
|
72
|
+
* reply is credited to.
|
|
73
|
+
*/
|
|
74
|
+
export const MAX_HELD_CLAIMS = 50;
|
|
75
|
+
export const MAX_PENDING_TRANSITIONS = 50;
|
|
76
|
+
export function emptyClaimState() {
|
|
77
|
+
return { held: [], pending: {} };
|
|
78
|
+
}
|
|
79
|
+
function cloneState(s) {
|
|
80
|
+
return {
|
|
81
|
+
held: [...s.held],
|
|
82
|
+
pending: Object.fromEntries(Object.entries(s.pending).map(([id, p]) => [id, { ...p }])),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** The server's answer to a `50arms_api` call is the JSON text `{"ok": true|false, "status": ...}`; only a leading `ok:true` is a success. */
|
|
86
|
+
const OK_TRUE = /^\s*\{\s*"ok"\s*:\s*true\b/;
|
|
87
|
+
function resultText(content) {
|
|
88
|
+
if (typeof content === "string")
|
|
89
|
+
return content;
|
|
90
|
+
if (!Array.isArray(content))
|
|
91
|
+
return "";
|
|
92
|
+
return content
|
|
93
|
+
.map((part) => {
|
|
94
|
+
const p = part;
|
|
95
|
+
return p !== null && typeof p === "object" && p.type === "text" && typeof p.text === "string" ? p.text : "";
|
|
96
|
+
})
|
|
97
|
+
.join("");
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Whether a `tool_result` block reports a successful `50arms_api` call. Anything
|
|
101
|
+
* that is not a leading `{"ok": true` is NOT a success: an errored tool call, a
|
|
102
|
+
* 409's `{"ok": false, ...}`, a refusal, text the tool never produces. A truncated
|
|
103
|
+
* body still starts with its `ok`, so a large answer reads correctly.
|
|
104
|
+
*/
|
|
105
|
+
function resultSucceeded(block) {
|
|
106
|
+
if (block.is_error === true)
|
|
107
|
+
return false;
|
|
108
|
+
return OK_TRUE.test(resultText(block.content));
|
|
109
|
+
}
|
|
110
|
+
/** The transition a `tool_use` block asks for, or `null` when it is not a claim or release of a card. */
|
|
111
|
+
function transitionOf(block) {
|
|
112
|
+
if (block.type !== "tool_use" || typeof block.name !== "string" || !DASHBOARD_API_TOOL_NAMES.has(block.name))
|
|
113
|
+
return null;
|
|
114
|
+
const input = block.input;
|
|
115
|
+
if (input === null || typeof input !== "object")
|
|
116
|
+
return null;
|
|
117
|
+
if (typeof input.method !== "string" || input.method.toUpperCase() !== "POST")
|
|
118
|
+
return null;
|
|
119
|
+
if (typeof input.path !== "string")
|
|
120
|
+
return null;
|
|
121
|
+
const match = TRANSITION_PATH.exec(input.path);
|
|
122
|
+
if (match === null)
|
|
123
|
+
return null;
|
|
124
|
+
const body = input.body;
|
|
125
|
+
if (body === null || typeof body !== "object" || typeof body.action !== "string")
|
|
126
|
+
return null;
|
|
127
|
+
if (match[1].length > ISSUE_ID_MAX_LENGTH)
|
|
128
|
+
return null;
|
|
129
|
+
if (body.action === CLAIM_ACTION)
|
|
130
|
+
return { op: "claim", issueId: match[1] };
|
|
131
|
+
if (RELEASE_ACTIONS.has(body.action))
|
|
132
|
+
return { op: "release", issueId: match[1] };
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The per-process claim tracker: one state per sub-agent source. Fed every
|
|
137
|
+
* transcript line by `DispatchUsage` (`operator-bridge` mode only — the worker never
|
|
138
|
+
* constructs one), seeded from the stored cursor at attach, and snapshotted per
|
|
139
|
+
* source for the stats write.
|
|
140
|
+
*/
|
|
141
|
+
export class ClaimTracker {
|
|
142
|
+
states = new Map();
|
|
143
|
+
/** Replace a source's state — at attach (the stored cursor's claims) and on a past-EOF reset (empty). */
|
|
144
|
+
seed(source, state) {
|
|
145
|
+
if (source === MAIN_SOURCE)
|
|
146
|
+
return;
|
|
147
|
+
this.states.set(source, cloneState(state));
|
|
148
|
+
}
|
|
149
|
+
/** A copy of the source's state as of the last line fed; empty for a source that holds nothing. */
|
|
150
|
+
snapshot(source) {
|
|
151
|
+
const state = this.states.get(source);
|
|
152
|
+
return state === undefined ? emptyClaimState() : cloneState(state);
|
|
153
|
+
}
|
|
154
|
+
/** The card a reply whose first line is read NOW is credited to; `null` when the source holds none. */
|
|
155
|
+
issueFor(source) {
|
|
156
|
+
const held = this.states.get(source)?.held;
|
|
157
|
+
return held === undefined || held.length === 0 ? null : held[held.length - 1];
|
|
158
|
+
}
|
|
159
|
+
/** An assistant line's content blocks: remember every claim / release the line asks for until its result arrives. */
|
|
160
|
+
observeAssistantBlocks(source, blocks) {
|
|
161
|
+
if (source === MAIN_SOURCE)
|
|
162
|
+
return;
|
|
163
|
+
for (const raw of blocks) {
|
|
164
|
+
const block = raw;
|
|
165
|
+
if (block === null || typeof block !== "object" || typeof block.id !== "string")
|
|
166
|
+
continue;
|
|
167
|
+
const transition = transitionOf(block);
|
|
168
|
+
if (transition === null)
|
|
169
|
+
continue;
|
|
170
|
+
const state = this.stateOf(source);
|
|
171
|
+
delete state.pending[block.id];
|
|
172
|
+
state.pending[block.id] = transition;
|
|
173
|
+
const ids = Object.keys(state.pending);
|
|
174
|
+
for (const stale of ids.slice(0, Math.max(0, ids.length - MAX_PENDING_TRANSITIONS)))
|
|
175
|
+
delete state.pending[stale];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** A user line's content blocks: settle every remembered transition whose result is here. */
|
|
179
|
+
observeResultBlocks(source, blocks) {
|
|
180
|
+
if (source === MAIN_SOURCE)
|
|
181
|
+
return;
|
|
182
|
+
const state = this.states.get(source);
|
|
183
|
+
if (state === undefined)
|
|
184
|
+
return;
|
|
185
|
+
for (const raw of blocks) {
|
|
186
|
+
const block = raw;
|
|
187
|
+
if (block === null || typeof block !== "object" || block.type !== "tool_result")
|
|
188
|
+
continue;
|
|
189
|
+
if (typeof block.tool_use_id !== "string")
|
|
190
|
+
continue;
|
|
191
|
+
const pending = state.pending[block.tool_use_id];
|
|
192
|
+
if (pending === undefined)
|
|
193
|
+
continue;
|
|
194
|
+
delete state.pending[block.tool_use_id];
|
|
195
|
+
if (!resultSucceeded(block))
|
|
196
|
+
continue;
|
|
197
|
+
state.held = state.held.filter((id) => id !== pending.issueId);
|
|
198
|
+
if (pending.op === "claim") {
|
|
199
|
+
state.held.push(pending.issueId);
|
|
200
|
+
if (state.held.length > MAX_HELD_CLAIMS)
|
|
201
|
+
state.held = state.held.slice(state.held.length - MAX_HELD_CLAIMS);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
stateOf(source) {
|
|
206
|
+
let state = this.states.get(source);
|
|
207
|
+
if (state === undefined) {
|
|
208
|
+
state = emptyClaimState();
|
|
209
|
+
this.states.set(source, state);
|
|
210
|
+
}
|
|
211
|
+
return state;
|
|
212
|
+
}
|
|
213
|
+
}
|