@zq-silk/yui 0.11.3 → 0.12.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.
Files changed (86) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +36 -13
  3. package/dist/cli/commandCatalog.js +2 -4
  4. package/dist/cli.js +105 -43
  5. package/dist/commands/configCommands.js +12 -45
  6. package/dist/commands/executionAuditCommands.js +1 -1
  7. package/dist/commands/jobCommands.js +4 -10
  8. package/dist/commands/taskCommands.js +154 -171
  9. package/dist/commands/taskCompletionGate.js +29 -54
  10. package/dist/commands/taskContextCommand.js +55 -11
  11. package/dist/commands/taskInputCommands.js +8 -5
  12. package/dist/commands/taskNextActionCommand.js +56 -6
  13. package/dist/commands/taskOverviewCommand.js +17 -31
  14. package/dist/commands/taskRoleRuntimeStatus.js +24 -0
  15. package/dist/commands/taskWorkspaceCommands.js +5 -2
  16. package/dist/config/configCatalog.js +2 -2
  17. package/dist/config/yuiConfig.js +3 -7
  18. package/dist/context/wakeNotification.js +20 -5
  19. package/dist/controller/agentRuntimeObserver.js +247 -51
  20. package/dist/controller/clientRuntime.js +1 -1
  21. package/dist/controller/controller.js +8 -12
  22. package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
  23. package/dist/controller/runtime.js +3 -3
  24. package/dist/controller/runtimeEventInbox.js +17 -7
  25. package/dist/controller/runtimeEventProcessor.js +12 -19
  26. package/dist/controller/runtimeObservationHook.js +45 -0
  27. package/dist/execution/resourceBroker.js +5 -4
  28. package/dist/executor/agentExecutor.js +4 -4
  29. package/dist/executor/effectiveLaunch.js +8 -48
  30. package/dist/executor/fileRoleLaunchPlanner.js +36 -16
  31. package/dist/executor/workspacePreflightClassification.js +23 -2
  32. package/dist/interaction/operatorPresentation.js +33 -89
  33. package/dist/lifecycle/exactRunTerminalization.js +1 -2
  34. package/dist/lifecycle/taskRoleSessionReset.js +5 -13
  35. package/dist/observability/orchestrationMetrics.js +8 -3
  36. package/dist/profile/agentProfile.js +1 -1
  37. package/dist/release/cliHomeReleaseFence.js +123 -0
  38. package/dist/repository/taskWorkspacePreparer.js +176 -60
  39. package/dist/resources/sqliteResourceRegistry.js +1 -1
  40. package/dist/review/deltaRecheck.js +12 -51
  41. package/dist/review/reviewAcceptance.js +26 -0
  42. package/dist/review/reviewConfig.js +0 -31
  43. package/dist/review/reviewDecision.js +113 -0
  44. package/dist/review/reviewOutcomeClassifier.js +1 -1
  45. package/dist/review/reviewRound.js +1 -1
  46. package/dist/review/reviewerAvailability.js +69 -0
  47. package/dist/runtime/agentDriverObservation.js +24 -10
  48. package/dist/runtime/builtinAgentDrivers.js +4 -3
  49. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  50. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  51. package/dist/runtime/index.js +1 -0
  52. package/dist/runtime/runtimeObservation.js +13 -0
  53. package/dist/runtime/runtimeProjection.js +16 -25
  54. package/dist/runtime/sessionTokenMetrics.js +181 -0
  55. package/dist/runtime/structuredProviderHost.js +7 -1
  56. package/dist/scheduler/activeRoleRunDelivery.js +4 -4
  57. package/dist/scheduler/activeTaskProgress.js +7 -6
  58. package/dist/scheduler/leaderWakeupProcessor.js +3 -5
  59. package/dist/scheduler/operatorEvent.js +34 -0
  60. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  61. package/dist/scheduler/roleRunStall.js +0 -14
  62. package/dist/scheduler/taskExecutionProjection.js +97 -76
  63. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  64. package/dist/storage/migration/productionRegistry.js +379 -0
  65. package/dist/storage/sqliteSchema.js +18 -8
  66. package/dist/storage/sqliteStore.js +24 -20
  67. package/dist/storage/storeRpc.js +0 -1
  68. package/dist/storage/taskStore.js +15 -30
  69. package/dist/storage/upgrade/recordVersions.js +3 -2
  70. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  71. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  72. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  73. package/dist/task/completionReadiness.js +10 -6
  74. package/dist/task/nextAction.js +82 -24
  75. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  76. package/dist/web/assets/client/components.js +13 -3
  77. package/dist/web/assets/client/i18n.js +6 -2
  78. package/dist/web/webSnapshot.js +4 -0
  79. package/i18n/README.zh-CN.md +2 -2
  80. package/package.json +1 -1
  81. package/skills/yui-leader/SKILL.md +53 -16
  82. package/skills/yui-operator/SKILL.md +9 -0
  83. package/skills/yui-reviewer/SKILL.md +28 -7
  84. package/dist/context/sessionContextBudget.js +0 -71
  85. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  86. package/dist/scheduler/operatorNotification.js +0 -59
@@ -62,7 +62,7 @@ export function classifyReviewRoundOutcome(round, evidence) {
62
62
  reason: corroborationFailure
63
63
  };
64
64
  }
65
- /** True only when a Round may consume semantic Review budget or feed findings. */
65
+ /** True only when a Round contains semantic Reviewer evidence. */
66
66
  export function isSemanticReviewRound(round, evidence) {
67
67
  return classifyReviewRoundOutcome(round, evidence)?.kind === "semantic";
68
68
  }
@@ -120,7 +120,7 @@ export function finishReviewRound(round, status, summary, now, result = {}) {
120
120
  return terminal;
121
121
  }
122
122
  // Fail closed: a completed delta Round without an explicit, valid
123
- // disposition escalates to a full Review. Uncertainty never accepts.
123
+ // disposition remains non-accepting for Leader routing. Uncertainty never accepts.
124
124
  const disposition = result.deltaDisposition ?? "requires-full-review";
125
125
  const reasoning = result.deltaReasoning ?? result.report ?? summary;
126
126
  return validateReviewRound({
@@ -0,0 +1,69 @@
1
+ import { nextPendingBatch, mailboxHasWork } from "../coordination/workMailbox.js";
2
+ import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
+ /** One authoritative read-only projection for a Task Reviewer Role slot. */
4
+ export function projectReviewerAvailability(store, taskId, reviewerRoleName) {
5
+ const active = store.getActiveAgentRun(taskId, reviewerRoleName)
6
+ ?? store.listAgentRuns(taskId).find((run) => (run.roleName === reviewerRoleName && run.status === "active"));
7
+ if (active !== null && active !== undefined) {
8
+ return {
9
+ kind: "busy",
10
+ reviewerRoleName,
11
+ phase: "active-run",
12
+ activeRunId: active.id,
13
+ ...(active.reviewRoundId === undefined
14
+ ? {}
15
+ : { activeReviewRoundId: active.reviewRoundId }),
16
+ startedAt: active.createdAt,
17
+ retryable: true,
18
+ retryAfterSeconds: 5
19
+ };
20
+ }
21
+ const activeRound = store.listReviewRounds(taskId).find((round) => ((round.scope ?? "work-item") === "task"
22
+ && (round.status === "pending" || round.status === "running")
23
+ && (round.reviewerRoleName === reviewerRoleName
24
+ || round.executionGroup?.lanes.some(({ roleName }) => roleName === reviewerRoleName) === true)));
25
+ if (activeRound !== undefined) {
26
+ return {
27
+ kind: "busy",
28
+ reviewerRoleName,
29
+ phase: "review-slot",
30
+ activeReviewRoundId: activeRound.id,
31
+ startedAt: activeRound.createdAt,
32
+ retryable: true,
33
+ retryAfterSeconds: 5
34
+ };
35
+ }
36
+ const reviewerMailbox = store.getWorkMailbox({
37
+ kind: "role",
38
+ taskId,
39
+ roleName: reviewerRoleName
40
+ });
41
+ if (reviewerMailbox !== null && mailboxHasWork(reviewerMailbox)) {
42
+ return mailboxBusy(reviewerRoleName, "mailbox", reviewerMailbox);
43
+ }
44
+ const runtimeMailbox = store.getWorkMailbox(runtimeLifecycleTarget({
45
+ scope: "task",
46
+ taskId,
47
+ roleName: reviewerRoleName
48
+ }));
49
+ if (runtimeMailbox !== null && mailboxHasWork(runtimeMailbox)) {
50
+ return mailboxBusy(reviewerRoleName, "runtime-lifecycle", runtimeMailbox);
51
+ }
52
+ return {
53
+ kind: "available",
54
+ reviewerRoleName,
55
+ retryable: true
56
+ };
57
+ }
58
+ function mailboxBusy(reviewerRoleName, phase, mailbox) {
59
+ const pending = nextPendingBatch(mailbox);
60
+ const startedAt = mailbox.processing?.startedAt ?? pending?.firstQueuedAt;
61
+ return {
62
+ kind: "busy",
63
+ reviewerRoleName,
64
+ phase,
65
+ ...(startedAt === undefined ? {} : { startedAt }),
66
+ retryable: true,
67
+ retryAfterSeconds: 5
68
+ };
69
+ }
@@ -33,18 +33,24 @@ export function mapAgentDriverUsage(input, usage) {
33
33
  });
34
34
  }
35
35
  function observation(input, mapped) {
36
- const eventId = hookEventId(input, mapped);
36
+ const requestOccurrenceId = mapped.kind === "activity.observed"
37
+ && mapped.payload.usage?.semantics === "request-context"
38
+ ? requireRequestOccurrenceId(mapped.payload.activityId)
39
+ : undefined;
40
+ const eventId = hookEventId(input, mapped, requestOccurrenceId);
37
41
  const fence = { ...input.fence, ...mapped.fence };
38
42
  return createRuntimeObservation({
39
43
  schemaVersion: 2,
40
44
  eventId,
41
- semanticKey: runtimeObservationSemanticKey({
42
- eventId,
43
- kind: mapped.kind,
44
- fence,
45
- ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
46
- payload: mapped.payload
47
- }),
45
+ semanticKey: requestOccurrenceId === undefined
46
+ ? runtimeObservationSemanticKey({
47
+ eventId,
48
+ kind: mapped.kind,
49
+ fence,
50
+ ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
51
+ payload: mapped.payload
52
+ })
53
+ : `provider-request:${eventId}`,
48
54
  kind: mapped.kind,
49
55
  authority: "provider-structured",
50
56
  receivedAt: input.receivedAt,
@@ -60,13 +66,21 @@ function requireMatchingDriver(input) {
60
66
  throw new Error("Agent Driver Hook fence does not match the selected Driver.");
61
67
  }
62
68
  }
63
- function hookEventId(input, mapped) {
69
+ function hookEventId(input, mapped, requestOccurrenceId) {
64
70
  const digest = createHash("sha256").update(JSON.stringify([
65
71
  input.driver.id,
66
72
  input.hookEventName,
67
73
  input.fence,
68
74
  mapped.kind,
69
- mapped.payload
75
+ ...(requestOccurrenceId === undefined
76
+ ? [mapped.payload]
77
+ : [{ requestOccurrenceId }])
70
78
  ])).digest("hex");
71
79
  return `runtime-observation-${digest}`;
72
80
  }
81
+ function requireRequestOccurrenceId(value) {
82
+ if (value === undefined || value.trim().length === 0 || value.includes("\0")) {
83
+ throw new Error("Request-context usage requires a stable activity id.");
84
+ }
85
+ return value.trim();
86
+ }
@@ -51,8 +51,8 @@ const STRUCTURED_CLI_CAPABILITIES = Object.freeze({
51
51
  preInputReadiness: "unavailable",
52
52
  promptAcceptance: "exact",
53
53
  turnLifecycle: "exact",
54
- // Model activity is inferred from usage deltas; these are the operation
55
- // boundaries the current Hook surfaces actually expose.
54
+ // Transcript sources may emit an explicit activity identity separately
55
+ // from usage snapshots. Numeric token values never decide runtime health.
56
56
  operations: Object.freeze(["tool", "subagent"]),
57
57
  waiting: Object.freeze(["permission"]),
58
58
  usage: "streaming-cumulative",
@@ -70,7 +70,8 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
70
70
  observation: Object.freeze({
71
71
  ...STRUCTURED_CLI_CAPABILITIES.observation,
72
72
  sessionBootstrap: "preallocated",
73
- preInputReadiness: "exact"
73
+ preInputReadiness: "exact",
74
+ usage: "event-snapshot"
74
75
  })
75
76
  }),
76
77
  runtime: Object.freeze({
@@ -5,6 +5,7 @@ const MAX_INITIAL_TAIL_BYTES = 1024 * 1024;
5
5
  const MAX_SAMPLE_BYTES = 1024 * 1024;
6
6
  const MAX_REMAINDER_BYTES = 64 * 1024;
7
7
  const MAX_CLAUDE_MESSAGES = 4_096;
8
+ const INITIAL_CONTINUITY_EPOCH = "initial";
8
9
  export function transcriptObserverSource(driverId, input) {
9
10
  if (input.hookEventName !== "UserPromptSubmit")
10
11
  return null;
@@ -12,12 +13,8 @@ export function transcriptObserverSource(driverId, input) {
12
13
  if (typeof locator !== "string" || !isAbsolute(locator) || locator.includes("\0"))
13
14
  return null;
14
15
  const sessionId = identity(input.payload.session_id) ?? "session";
15
- const turnId = identity(input.payload.prompt_id)
16
- ?? identity(input.payload.turn_id)
17
- ?? input.occurrenceId
18
- ?? "turn";
19
16
  const digest = createHash("sha256")
20
- .update(JSON.stringify([driverId, sessionId, turnId, locator]))
17
+ .update(JSON.stringify([driverId, sessionId, locator]))
21
18
  .digest("hex");
22
19
  return Object.freeze({
23
20
  schemaVersion: 1,
@@ -26,13 +23,13 @@ export function transcriptObserverSource(driverId, input) {
26
23
  locator
27
24
  });
28
25
  }
29
- export async function codexTranscriptObserver(source, cursor) {
30
- return sampleJsonl(source, cursor, parseCodexLines);
26
+ export async function codexTranscriptObserver(source, cursor, resume) {
27
+ return sampleJsonl(source, cursor, resume, parseCodexLines);
31
28
  }
32
- export async function claudeTranscriptObserver(source, cursor) {
33
- return sampleJsonl(source, cursor, parseClaudeLines);
29
+ export async function claudeTranscriptObserver(source, cursor, resume) {
30
+ return sampleJsonl(source, cursor, resume, parseClaudeLines);
34
31
  }
35
- async function sampleJsonl(source, rawCursor, parse) {
32
+ async function sampleJsonl(source, rawCursor, resume, parse) {
36
33
  const previous = normalizeCursor(rawCursor);
37
34
  let metadata;
38
35
  try {
@@ -43,8 +40,33 @@ async function sampleJsonl(source, rawCursor, parse) {
43
40
  }
44
41
  if (!metadata.isFile())
45
42
  return unavailable(previous, new Error("observer locator is not a file"));
46
- const reset = previous !== undefined && metadata.size < previous.offset;
43
+ const fileFingerprint = transcriptFileFingerprint(metadata);
44
+ const checkpoint = previous === undefined
45
+ ? transcriptCheckpoint(resume?.latestCheckpoint, resume?.latestOccurrenceId)
46
+ : undefined;
47
+ let checkpointMatches = false;
48
+ if (previous === undefined && checkpoint !== undefined) {
49
+ try {
50
+ checkpointMatches = await matchesTranscriptCheckpoint(source.locator, metadata, fileFingerprint, checkpoint);
51
+ }
52
+ catch (error) {
53
+ return unavailable(previous, error);
54
+ }
55
+ }
56
+ const reset = previous !== undefined && (previous.fileFingerprint === undefined
57
+ || previous.fileFingerprint !== fileFingerprint
58
+ || metadata.size < previous.offset
59
+ || (metadata.size === previous.offset && (previous.fileCtimeMs === undefined
60
+ || previous.fileCtimeMs !== metadata.ctimeMs)));
61
+ const offlineReset = previous === undefined
62
+ && checkpoint !== undefined
63
+ && !checkpointMatches;
47
64
  const initial = previous === undefined || reset;
65
+ const continuityEpoch = reset || offlineReset
66
+ ? rotateContinuityEpoch(previous?.continuityEpoch ?? checkpoint?.continuityEpoch ?? INITIAL_CONTINUITY_EPOCH, fileFingerprint)
67
+ : previous?.continuityEpoch
68
+ ?? checkpoint?.continuityEpoch
69
+ ?? initialContinuityEpoch(fileFingerprint);
48
70
  const start = initial
49
71
  ? Math.max(0, metadata.size - MAX_INITIAL_TAIL_BYTES)
50
72
  : previous.offset;
@@ -67,32 +89,80 @@ async function sampleJsonl(source, rawCursor, parse) {
67
89
  return unavailable(previous, error);
68
90
  }
69
91
  let text = `${initial ? "" : previous?.remainder ?? ""}${bytes.toString("utf8")}`;
92
+ let textOffset = initial
93
+ ? start
94
+ : previous.offset - Buffer.byteLength(previous.remainder, "utf8");
70
95
  if (initial && start > 0) {
71
96
  const firstLineEnd = text.indexOf("\n");
72
- text = firstLineEnd < 0 ? "" : text.slice(firstLineEnd + 1);
97
+ if (firstLineEnd < 0) {
98
+ textOffset += Buffer.byteLength(text, "utf8");
99
+ text = "";
100
+ }
101
+ else {
102
+ const discarded = text.slice(0, firstLineEnd + 1);
103
+ textOffset += Buffer.byteLength(discarded, "utf8");
104
+ text = text.slice(firstLineEnd + 1);
105
+ }
73
106
  }
74
107
  const complete = text.endsWith("\n");
75
108
  const split = text.split("\n");
76
109
  const remainder = complete ? "" : split.pop() ?? "";
110
+ if (complete)
111
+ split.pop();
112
+ let lineOffset = textOffset;
113
+ const lines = split.map((content) => {
114
+ const line = Object.freeze({
115
+ content,
116
+ offset: lineOffset,
117
+ continuityEpoch,
118
+ fileFingerprint,
119
+ observedFileSize: metadata.size,
120
+ observedFileCtimeMs: metadata.ctimeMs
121
+ });
122
+ lineOffset += Buffer.byteLength(content, "utf8") + 1;
123
+ return line;
124
+ });
77
125
  const boundedRemainder = Buffer.byteLength(remainder, "utf8") <= MAX_REMAINDER_BYTES
78
126
  ? remainder
79
127
  : "";
80
- const parsed = parse(split, initial ? {} : previous?.state ?? {});
128
+ const parsed = parse(lines, initial ? {} : previous?.state ?? {});
129
+ const clippedBaseline = initial && start > 0;
130
+ const discontinuousBaseline = clippedBaseline || reset || offlineReset;
131
+ // A verified checkpoint inside this bounded read proves continuity only for
132
+ // the occurrences that follow it. Everything earlier remains partial.
133
+ const recoveredCheckpointIndex = checkpointMatches && checkpoint !== undefined
134
+ ? parsed.usages.findIndex(({ occurrenceId }) => occurrenceId === checkpoint.occurrenceId)
135
+ : -1;
136
+ const usages = discontinuousBaseline
137
+ ? parsed.usages.map((occurrence, index) => (recoveredCheckpointIndex >= 0 && index > recoveredCheckpointIndex
138
+ ? occurrence
139
+ : Object.freeze({
140
+ ...occurrence,
141
+ observationQuality: "partial"
142
+ })))
143
+ : parsed.usages;
81
144
  const nextCursor = Object.freeze({
82
145
  offset: start + bytes.length,
83
146
  remainder: boundedRemainder,
84
- state: parsed.state
147
+ state: parsed.state,
148
+ continuityEpoch,
149
+ fileFingerprint,
150
+ fileCtimeMs: metadata.ctimeMs
85
151
  });
86
152
  const fellBehind = start + bytes.length < metadata.size;
87
153
  const detail = parsed.degraded
154
+ ?? (clippedBaseline
155
+ ? "Initial transcript history was clipped; request-boundary evidence is partial."
156
+ : undefined)
88
157
  ?? (fellBehind ? "Transcript observer is catching up with a bounded read." : undefined)
158
+ ?? (offlineReset ? "Transcript changed while the observer was offline; baseline was reset." : undefined)
89
159
  ?? (reset ? "Transcript was truncated; observer baseline was reset." : undefined)
90
160
  ?? (remainder !== boundedRemainder ? "Oversized partial transcript line was discarded." : undefined);
91
161
  return Object.freeze({
92
162
  cursor: nextCursor,
93
163
  status: detail === undefined ? "healthy" : "degraded",
94
164
  ...(detail === undefined ? {} : { detail }),
95
- ...(parsed.usage === undefined ? {} : { usage: parsed.usage }),
165
+ ...(usages.length === 0 ? {} : { usages: Object.freeze(usages) }),
96
166
  ...(parsed.activityId === undefined ? {} : {
97
167
  activity: "model",
98
168
  activityId: parsed.activityId
@@ -101,12 +171,17 @@ async function sampleJsonl(source, rawCursor, parse) {
101
171
  }
102
172
  function parseCodexLines(lines, state) {
103
173
  let usage = usageFrom(state.usage);
104
- let activityId;
174
+ // Keep every recognized-but-incomplete request boundary durable. Reuse the
175
+ // last complete cumulative total as a partial marker until the next exact
176
+ // occurrence restores the numeric counter.
177
+ let usageContinuityGap = state.usageContinuityGap === true;
178
+ const usages = [];
105
179
  let malformed = 0;
180
+ let incompleteUsage = 0;
106
181
  for (const line of lines) {
107
- const entry = jsonObject(line);
182
+ const entry = jsonObject(line.content);
108
183
  if (entry === null) {
109
- if (line.trim().length > 0)
184
+ if (line.content.trim().length > 0)
110
185
  malformed += 1;
111
186
  continue;
112
187
  }
@@ -115,27 +190,51 @@ function parseCodexLines(lines, state) {
115
190
  const payload = object(entry.payload);
116
191
  if (payload?.type !== "token_count")
117
192
  continue;
193
+ // Codex also emits token_count records that carry rate limits only. They
194
+ // contain no usage occurrence and therefore create no request boundary.
195
+ if (payload.info === null)
196
+ continue;
118
197
  const candidate = normalizedCodexUsage(object(object(payload.info)?.total_token_usage));
119
- if (candidate === undefined)
198
+ if (candidate === undefined) {
199
+ usages.push(Object.freeze({
200
+ ...transcriptOccurrence(line),
201
+ observationQuality: "partial",
202
+ ...(usage === undefined ? {} : { usage })
203
+ }));
204
+ usageContinuityGap = true;
205
+ incompleteUsage += 1;
120
206
  continue;
207
+ }
121
208
  usage = candidate;
122
- activityId = `usage:${candidate.inputTokens}:${candidate.outputTokens}`;
209
+ usages.push(Object.freeze({
210
+ ...transcriptOccurrence(line),
211
+ ...(usageContinuityGap ? { observationQuality: "partial" } : {}),
212
+ usage: candidate
213
+ }));
214
+ usageContinuityGap = false;
123
215
  }
216
+ const degraded = transcriptParseDetail(malformed, incompleteUsage);
124
217
  return Object.freeze({
125
- state: Object.freeze({ ...(usage === undefined ? {} : { usage }) }),
126
- ...(usage === undefined ? {} : { usage }),
127
- ...(activityId === undefined ? {} : { activityId }),
128
- ...(malformed === 0 ? {} : { degraded: `${malformed} malformed transcript line(s) ignored.` })
218
+ state: Object.freeze({
219
+ ...(usage === undefined ? {} : { usage }),
220
+ ...(usageContinuityGap ? { usageContinuityGap: true } : {})
221
+ }),
222
+ usages: Object.freeze(usages),
223
+ ...(degraded === undefined ? {} : { degraded })
129
224
  });
130
225
  }
131
226
  function parseClaudeLines(lines, state) {
132
227
  const messages = messageState(state.messages);
228
+ let usageContinuityGap = state.usageContinuityGap === true;
229
+ const usages = [];
133
230
  let activityId;
134
231
  let malformed = 0;
232
+ let incompleteUsage = 0;
233
+ let bounded = false;
135
234
  for (const line of lines) {
136
- const entry = jsonObject(line);
235
+ const entry = jsonObject(line.content);
137
236
  if (entry === null) {
138
- if (line.trim().length > 0)
237
+ if (line.content.trim().length > 0)
139
238
  malformed += 1;
140
239
  continue;
141
240
  }
@@ -143,50 +242,209 @@ function parseClaudeLines(lines, state) {
143
242
  continue;
144
243
  const message = object(entry.message);
145
244
  const usage = normalizedClaudeUsage(object(message?.usage));
146
- if (usage === undefined)
245
+ if (usage === undefined) {
246
+ usages.push(Object.freeze({
247
+ ...transcriptOccurrence(line),
248
+ observationQuality: "partial"
249
+ }));
250
+ usageContinuityGap = true;
251
+ incompleteUsage += 1;
147
252
  continue;
253
+ }
148
254
  const key = identity(message?.id) === undefined
149
255
  ? identity(entry.uuid) === undefined ? undefined : `entry:${identity(entry.uuid)}`
150
256
  : `message:${identity(message?.id)}`;
151
- if (key === undefined)
257
+ if (key === undefined) {
258
+ usages.push(Object.freeze({
259
+ ...transcriptOccurrence(line),
260
+ observationQuality: "partial"
261
+ }));
262
+ usageContinuityGap = true;
263
+ incompleteUsage += 1;
152
264
  continue;
265
+ }
153
266
  messages[key] = usage;
154
- activityId = `${key}:${usage.inputTokens}:${usage.outputTokens}`;
155
- }
156
- const keys = Object.keys(messages);
157
- let degraded = malformed === 0 ? undefined : `${malformed} malformed transcript line(s) ignored.`;
158
- if (keys.length > MAX_CLAUDE_MESSAGES) {
159
- for (const key of keys.slice(0, keys.length - MAX_CLAUDE_MESSAGES))
160
- delete messages[key];
161
- degraded = "Claude transcript message baseline was bounded to the newest entries.";
267
+ activityId = key;
268
+ const keys = Object.keys(messages);
269
+ if (keys.length > MAX_CLAUDE_MESSAGES) {
270
+ for (const oldKey of keys.slice(0, keys.length - MAX_CLAUDE_MESSAGES)) {
271
+ delete messages[oldKey];
272
+ }
273
+ bounded = true;
274
+ }
275
+ usages.push(Object.freeze({
276
+ ...transcriptOccurrence(line),
277
+ activityId: key,
278
+ ...(usageContinuityGap ? { observationQuality: "partial" } : {}),
279
+ usage
280
+ }));
281
+ usageContinuityGap = false;
162
282
  }
163
- const usage = sumUsage(Object.values(messages));
283
+ const degraded = transcriptParseDetail(malformed, incompleteUsage, bounded);
164
284
  return Object.freeze({
165
- state: Object.freeze({ messages: Object.freeze(messages) }),
166
- ...(usage === undefined ? {} : { usage }),
285
+ state: Object.freeze({
286
+ messages: Object.freeze(messages),
287
+ ...(usageContinuityGap ? { usageContinuityGap: true } : {})
288
+ }),
289
+ usages: Object.freeze(usages),
167
290
  ...(activityId === undefined ? {} : { activityId }),
168
291
  ...(degraded === undefined ? {} : { degraded })
169
292
  });
170
293
  }
294
+ function transcriptParseDetail(malformed, incompleteUsage, bounded = false) {
295
+ const details = [];
296
+ if (malformed > 0)
297
+ details.push(`${malformed} malformed transcript line(s) ignored.`);
298
+ if (incompleteUsage > 0) {
299
+ details.push(`${incompleteUsage} incomplete usage record(s) created a request-boundary gap.`);
300
+ }
301
+ if (bounded)
302
+ details.push("Claude transcript message baseline was bounded to the newest entries.");
303
+ return details.length === 0 ? undefined : details.join(" ");
304
+ }
305
+ function transcriptOccurrence(line) {
306
+ // The durable semantic key retains this bounded checkpoint. A Controller
307
+ // restart can therefore reuse the epoch only when the sampled file is still
308
+ // the same continuity, without persisting the process-local cursor itself.
309
+ const digest = createHash("sha256").update(line.content).digest("hex");
310
+ const occurrenceId = `transcript-v1:${Buffer.from(JSON.stringify({
311
+ continuityEpoch: line.continuityEpoch,
312
+ fileFingerprint: line.fileFingerprint,
313
+ offset: line.offset,
314
+ digest
315
+ }), "utf8").toString("base64url")}`;
316
+ const resumeCheckpoint = `transcript-checkpoint-v1:${Buffer.from(JSON.stringify({
317
+ occurrenceId,
318
+ continuityEpoch: line.continuityEpoch,
319
+ fileFingerprint: line.fileFingerprint,
320
+ offset: line.offset,
321
+ length: Buffer.byteLength(line.content, "utf8"),
322
+ digest,
323
+ observedFileSize: line.observedFileSize,
324
+ observedFileCtimeMs: line.observedFileCtimeMs
325
+ }), "utf8").toString("base64url")}`;
326
+ return Object.freeze({ occurrenceId, resumeCheckpoint });
327
+ }
328
+ function initialContinuityEpoch(fileFingerprint) {
329
+ return continuityEpoch("initial", fileFingerprint);
330
+ }
331
+ function rotateContinuityEpoch(previous, fileFingerprint) {
332
+ return continuityEpoch(previous, fileFingerprint);
333
+ }
334
+ function continuityEpoch(previous, fileFingerprint) {
335
+ return `sha256:${createHash("sha256")
336
+ .update(JSON.stringify([previous, fileFingerprint]))
337
+ .digest("hex")}`;
338
+ }
339
+ function transcriptFileFingerprint(metadata) {
340
+ return `sha256:${createHash("sha256").update(JSON.stringify([
341
+ metadata.dev,
342
+ metadata.ino
343
+ ])).digest("hex")}`;
344
+ }
345
+ async function matchesTranscriptCheckpoint(locator, metadata, fileFingerprint, checkpoint) {
346
+ if (checkpoint.fileFingerprint !== fileFingerprint
347
+ || metadata.size < checkpoint.observedFileSize)
348
+ return false;
349
+ if (metadata.size === checkpoint.observedFileSize) {
350
+ return metadata.ctimeMs === checkpoint.observedFileCtimeMs;
351
+ }
352
+ if (checkpoint.offset + checkpoint.length + 1 > metadata.size)
353
+ return false;
354
+ const handle = await open(locator, "r");
355
+ try {
356
+ const bytes = Buffer.alloc(checkpoint.length + 1);
357
+ const read = await handle.read(bytes, 0, bytes.length, checkpoint.offset);
358
+ return read.bytesRead === bytes.length
359
+ && bytes.at(-1) === 0x0a
360
+ && createHash("sha256")
361
+ .update(bytes.subarray(0, checkpoint.length))
362
+ .digest("hex") === checkpoint.digest;
363
+ }
364
+ finally {
365
+ await handle.close();
366
+ }
367
+ }
368
+ function transcriptCheckpoint(raw, occurrenceId) {
369
+ const prefix = "transcript-checkpoint-v1:";
370
+ if (raw === undefined || occurrenceId === undefined || !raw.startsWith(prefix))
371
+ return undefined;
372
+ try {
373
+ const value = JSON.parse(Buffer.from(raw.slice(prefix.length), "base64url").toString("utf8"));
374
+ if (value === null || typeof value !== "object" || Array.isArray(value))
375
+ return undefined;
376
+ const checkpoint = value;
377
+ return checkpoint.occurrenceId !== occurrenceId
378
+ || typeof checkpoint.continuityEpoch !== "string"
379
+ || checkpoint.continuityEpoch.length === 0
380
+ || typeof checkpoint.fileFingerprint !== "string"
381
+ || checkpoint.fileFingerprint.length === 0
382
+ || !Number.isSafeInteger(checkpoint.offset)
383
+ || checkpoint.offset < 0
384
+ || !Number.isSafeInteger(checkpoint.length)
385
+ || checkpoint.length < 0
386
+ || typeof checkpoint.digest !== "string"
387
+ || checkpoint.digest.length === 0
388
+ || !Number.isSafeInteger(checkpoint.observedFileSize)
389
+ || checkpoint.observedFileSize < 0
390
+ || typeof checkpoint.observedFileCtimeMs !== "number"
391
+ || !Number.isFinite(checkpoint.observedFileCtimeMs)
392
+ || checkpoint.observedFileCtimeMs < 0
393
+ ? undefined
394
+ : Object.freeze({
395
+ occurrenceId,
396
+ continuityEpoch: checkpoint.continuityEpoch,
397
+ fileFingerprint: checkpoint.fileFingerprint,
398
+ offset: checkpoint.offset,
399
+ length: checkpoint.length,
400
+ digest: checkpoint.digest,
401
+ observedFileSize: checkpoint.observedFileSize,
402
+ observedFileCtimeMs: checkpoint.observedFileCtimeMs
403
+ });
404
+ }
405
+ catch {
406
+ return undefined;
407
+ }
408
+ }
171
409
  function normalizeCursor(input) {
172
410
  if (input === undefined)
173
411
  return undefined;
174
412
  const offset = input.offset;
175
413
  const remainder = input.remainder;
176
414
  const state = input.state;
415
+ const continuityEpoch = input.continuityEpoch;
416
+ const fileFingerprint = input.fileFingerprint;
417
+ const fileCtimeMs = input.fileCtimeMs;
177
418
  if (!Number.isSafeInteger(offset) || offset < 0
178
419
  || typeof remainder !== "string"
179
- || state === null || typeof state !== "object" || Array.isArray(state))
420
+ || state === null || typeof state !== "object" || Array.isArray(state)
421
+ || (continuityEpoch !== undefined
422
+ && (typeof continuityEpoch !== "string" || continuityEpoch.length === 0))
423
+ || (fileFingerprint !== undefined
424
+ && (typeof fileFingerprint !== "string" || fileFingerprint.length === 0))
425
+ || (fileCtimeMs !== undefined
426
+ && (typeof fileCtimeMs !== "number" || !Number.isFinite(fileCtimeMs) || fileCtimeMs < 0))) {
180
427
  return undefined;
428
+ }
181
429
  return Object.freeze({
182
430
  offset: offset,
183
431
  remainder,
184
- state: state
432
+ state: state,
433
+ continuityEpoch: typeof continuityEpoch === "string"
434
+ ? continuityEpoch
435
+ : INITIAL_CONTINUITY_EPOCH,
436
+ ...(typeof fileFingerprint === "string" ? { fileFingerprint } : {}),
437
+ ...(typeof fileCtimeMs === "number" ? { fileCtimeMs } : {})
185
438
  });
186
439
  }
187
440
  function unavailable(cursor, error) {
188
441
  return Object.freeze({
189
- cursor: cursor ?? Object.freeze({ offset: 0, remainder: "", state: Object.freeze({}) }),
442
+ cursor: cursor ?? Object.freeze({
443
+ offset: 0,
444
+ remainder: "",
445
+ state: Object.freeze({}),
446
+ continuityEpoch: INITIAL_CONTINUITY_EPOCH
447
+ }),
190
448
  status: "unavailable",
191
449
  detail: error instanceof Error ? error.message : String(error)
192
450
  });
@@ -215,7 +473,7 @@ function normalizedClaudeUsage(usage) {
215
473
  const cacheCreated = integer(usage?.cache_creation_input_tokens) ?? 0;
216
474
  const reasoningTokens = integer(object(usage?.output_tokens_details)?.thinking_tokens);
217
475
  return Object.freeze({
218
- semantics: "cumulative-session",
476
+ semantics: "request-context",
219
477
  inputTokens: directInput + cacheRead + cacheCreated,
220
478
  outputTokens,
221
479
  cachedInputTokens: cacheRead + cacheCreated,
@@ -248,27 +506,6 @@ function usageFrom(value) {
248
506
  ...(reasoningTokens === undefined ? {} : { reasoningTokens })
249
507
  });
250
508
  }
251
- function sumUsage(values) {
252
- if (values.length === 0)
253
- return undefined;
254
- let inputTokens = 0;
255
- let outputTokens = 0;
256
- let cachedInputTokens = 0;
257
- let reasoningTokens = 0;
258
- for (const usage of values) {
259
- inputTokens += usage.inputTokens;
260
- outputTokens += usage.outputTokens;
261
- cachedInputTokens += usage.cachedInputTokens ?? 0;
262
- reasoningTokens += usage.reasoningTokens ?? 0;
263
- }
264
- return Object.freeze({
265
- semantics: "cumulative-session",
266
- inputTokens,
267
- outputTokens,
268
- cachedInputTokens,
269
- ...(reasoningTokens === 0 ? {} : { reasoningTokens })
270
- });
271
- }
272
509
  function jsonObject(line) {
273
510
  if (line.trim().length === 0)
274
511
  return null;