@tt-a1i/openpi 0.4.0 → 0.6.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 (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -0,0 +1,599 @@
1
+ import type {
2
+ AgentRecord,
3
+ AgentUsage,
4
+ WorkflowDetails,
5
+ WorkflowMemoryProjection,
6
+ } from "./model.ts";
7
+ import { toSerializable } from "./serialization.ts";
8
+
9
+ /** Defaults apply only to settled session-memory projections. Disk is canonical. */
10
+ export const DEFAULT_WORKFLOW_SETTLED_MAX_RUNS = 32;
11
+ export const DEFAULT_WORKFLOW_SETTLED_MAX_BYTES = 2 * 1024 * 1024;
12
+
13
+ const MAX_PHASES = 32;
14
+ const MAX_LOGS = 8;
15
+ const MAX_NAME_BYTES = 512;
16
+ const MAX_DESCRIPTION_BYTES = 1_024;
17
+ const MAX_LABEL_BYTES = 256;
18
+ const MAX_PREVIEW_BYTES = 512;
19
+ const MAX_ERROR_BYTES = 1_024;
20
+
21
+ export interface WorkflowSettledRunRetentionOptions {
22
+ /** Maximum number of settled projections retained in this session. */
23
+ readonly maxRuns?: number;
24
+ /** Maximum serialized UTF-8 bytes retained by all projections. */
25
+ readonly maxBytes?: number;
26
+ }
27
+
28
+ export interface WorkflowRetentionStats {
29
+ /** These counters describe only the current process/session epoch. */
30
+ readonly scope: "current-session";
31
+ readonly retainedRuns: number;
32
+ readonly retainedBytes: number;
33
+ /** Cumulative runs removed by count/byte pressure in this session. */
34
+ readonly evictedRuns: number;
35
+ /** Compatibility alias for evictedRuns. */
36
+ readonly settledRunsEvicted: number;
37
+ /** Serialized bytes belonging to projections removed by pressure. */
38
+ readonly evictedBytes: number;
39
+ }
40
+
41
+ function utf8Head(value: string, maxBytes: number) {
42
+ if (maxBytes <= 0) return "";
43
+ const bytes = Buffer.from(value, "utf8");
44
+ if (bytes.length <= maxBytes) return value;
45
+ let end = maxBytes;
46
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
47
+ return bytes.subarray(0, end).toString("utf8");
48
+ }
49
+
50
+ function bounded(value: string | undefined, maxBytes: number) {
51
+ return value === undefined ? undefined : utf8Head(value, maxBytes);
52
+ }
53
+
54
+ function jsonBytes(value: unknown) {
55
+ const serialized = JSON.stringify(toSerializable(value));
56
+ if (serialized === undefined)
57
+ throw new Error("Workflow projection is not serializable");
58
+ return Buffer.byteLength(serialized, "utf8");
59
+ }
60
+
61
+ function validateLimit(value: number, name: string) {
62
+ if (!Number.isSafeInteger(value) || value < 0) {
63
+ throw new Error(`${name} must be a non-negative safe integer`);
64
+ }
65
+ return value;
66
+ }
67
+
68
+ function finite(value: number | undefined) {
69
+ return typeof value === "number" && Number.isFinite(value)
70
+ ? value
71
+ : undefined;
72
+ }
73
+
74
+ function compactUsage(usage: AgentUsage) {
75
+ return {
76
+ input: finite(usage.input) ?? 0,
77
+ output: finite(usage.output) ?? 0,
78
+ cacheRead: finite(usage.cacheRead) ?? 0,
79
+ cacheWrite: finite(usage.cacheWrite) ?? 0,
80
+ cost: finite(usage.cost) ?? 0,
81
+ ...(finite(usage.contextTokens) !== undefined
82
+ ? { contextTokens: usage.contextTokens }
83
+ : {}),
84
+ turns: finite(usage.turns) ?? 0,
85
+ };
86
+ }
87
+
88
+ function compactInvocation(agent: AgentRecord) {
89
+ const invocation = agent.invocation;
90
+ if (!invocation) return undefined;
91
+ return {
92
+ identity: {
93
+ runId: invocation.identity.runId,
94
+ callIndex: invocation.identity.callIndex,
95
+ },
96
+ intentState: invocation.intentState,
97
+ admissionState: invocation.admissionState,
98
+ executionState: invocation.executionState,
99
+ ...(invocation.outcome ? { outcome: invocation.outcome } : {}),
100
+ requestedAt: invocation.requestedAt,
101
+ ...(invocation.claimedAt !== undefined
102
+ ? { claimedAt: invocation.claimedAt }
103
+ : {}),
104
+ ...(invocation.runningAt !== undefined
105
+ ? { runningAt: invocation.runningAt }
106
+ : {}),
107
+ ...(invocation.terminalAt !== undefined
108
+ ? { terminalAt: invocation.terminalAt }
109
+ : {}),
110
+ };
111
+ }
112
+
113
+ function compactAgent(
114
+ agent: AgentRecord,
115
+ display: boolean,
116
+ ): Record<string, unknown> {
117
+ const result: Record<string, unknown> = {
118
+ index: agent.index,
119
+ ...(agent.callId ? { callId: agent.callId } : {}),
120
+ ...(agent.invocation ? { invocation: compactInvocation(agent) } : {}),
121
+ label: bounded(agent.label, MAX_LABEL_BYTES) ?? "agent",
122
+ ...(agent.phase ? { phase: bounded(agent.phase, MAX_LABEL_BYTES) } : {}),
123
+ state: agent.state,
124
+ ...(agent.model ? { model: bounded(agent.model, MAX_LABEL_BYTES) } : {}),
125
+ ...(agent.contextWindow !== undefined
126
+ ? { contextWindow: agent.contextWindow }
127
+ : {}),
128
+ startedAt: agent.startedAt,
129
+ ...(agent.finishedAt !== undefined ? { finishedAt: agent.finishedAt } : {}),
130
+ usage: compactUsage(agent.usage),
131
+ transcript: [],
132
+ };
133
+
134
+ // These are recovery references, not display text. Truncating them would
135
+ // turn an otherwise recoverable artifact into an unusable path.
136
+ if (agent.resultArtifact) result.resultArtifact = agent.resultArtifact;
137
+ if (agent.resultRef) result.resultRef = agent.resultRef;
138
+
139
+ if (!display) return result;
140
+ if (agent.preview) result.preview = bounded(agent.preview, MAX_PREVIEW_BYTES);
141
+ if (agent.error) result.error = bounded(agent.error, MAX_ERROR_BYTES);
142
+ if (agent.replayed) result.replayed = true;
143
+ if (agent.operatorKey) result.operatorKey = agent.operatorKey;
144
+ if (agent.inputCallIds?.length)
145
+ result.inputCallIds = agent.inputCallIds.slice(0, 32);
146
+ if (agent.worktreeBranch) result.worktreeBranch = agent.worktreeBranch;
147
+ if (agent.worktreePath) result.worktreePath = agent.worktreePath;
148
+ if (agent.worktreeHandoffArtifact)
149
+ result.worktreeHandoffArtifact = agent.worktreeHandoffArtifact;
150
+ if (agent.worktreeCleanup) {
151
+ result.worktreeCleanup = {
152
+ removed: agent.worktreeCleanup.removed,
153
+ branchDeleted: agent.worktreeCleanup.branchDeleted,
154
+ branch: agent.worktreeCleanup.branch,
155
+ detached: agent.worktreeCleanup.detached,
156
+ ...(agent.worktreeCleanup.reason
157
+ ? { reason: bounded(agent.worktreeCleanup.reason, MAX_ERROR_BYTES) }
158
+ : {}),
159
+ };
160
+ }
161
+ if (agent.acceptance) {
162
+ result.acceptance = {
163
+ status: agent.acceptance.status,
164
+ errors: agent.acceptance.errors
165
+ .slice(0, 4)
166
+ .map((error) => bounded(error, MAX_ERROR_BYTES)),
167
+ criteria: agent.acceptance.criteria.slice(0, 16).map((criterion) => ({
168
+ id: bounded(criterion.id, MAX_LABEL_BYTES),
169
+ status: criterion.status,
170
+ evidence: criterion.evidence
171
+ .slice(0, 4)
172
+ .map((evidence) => bounded(evidence, MAX_PREVIEW_BYTES)),
173
+ ...(criterion.note
174
+ ? { note: bounded(criterion.note, MAX_ERROR_BYTES) }
175
+ : {}),
176
+ })),
177
+ };
178
+ }
179
+ return result;
180
+ }
181
+
182
+ function omissionMetadata(
183
+ details: WorkflowDetails,
184
+ agentLimit: number,
185
+ logLimit: number,
186
+ ): WorkflowMemoryProjection["omitted"] {
187
+ return {
188
+ agents: Math.max(0, details.agents.length - agentLimit),
189
+ logs:
190
+ Math.max(0, (details.logs?.length ?? 0) - logLimit) +
191
+ (details.logsDropped ?? 0),
192
+ transcriptEntries: details.agents.reduce(
193
+ (total, agent) => total + agent.transcript.length,
194
+ 0,
195
+ ),
196
+ result: details.result !== undefined,
197
+ graph: details.graph !== undefined,
198
+ };
199
+ }
200
+
201
+ function makeProjection(
202
+ details: WorkflowDetails,
203
+ cap: number,
204
+ agents: readonly Record<string, unknown>[],
205
+ logs: readonly unknown[],
206
+ agentLimit: number,
207
+ logLimit: number,
208
+ ) {
209
+ const phases = details.phases.slice(0, MAX_PHASES).map((phase) => ({
210
+ title: bounded(phase.title, MAX_LABEL_BYTES) ?? "phase",
211
+ ...(phase.detail
212
+ ? { detail: bounded(phase.detail, MAX_DESCRIPTION_BYTES) }
213
+ : {}),
214
+ }));
215
+ const omitted = omissionMetadata(details, agentLimit, logLimit);
216
+ const candidate: Record<string, unknown> = {
217
+ runId: details.runId,
218
+ ...(details.sessionId ? { sessionId: details.sessionId } : {}),
219
+ ...(details.name ? { name: bounded(details.name, MAX_NAME_BYTES) } : {}),
220
+ ...(details.description
221
+ ? { description: bounded(details.description, MAX_DESCRIPTION_BYTES) }
222
+ : {}),
223
+ background: details.background,
224
+ status: details.status,
225
+ startedAt: details.startedAt,
226
+ ...(details.finishedAt !== undefined
227
+ ? { finishedAt: details.finishedAt }
228
+ : {}),
229
+ phases,
230
+ ...(details.currentPhase
231
+ ? { currentPhase: bounded(details.currentPhase, MAX_LABEL_BYTES) }
232
+ : {}),
233
+ agents,
234
+ ...(logs.length > 0 ? { logs } : {}),
235
+ ...(details.logsDropped ? { logsDropped: details.logsDropped } : {}),
236
+ ...(details.delivery
237
+ ? {
238
+ delivery: {
239
+ id: details.delivery.id,
240
+ ...(details.delivery.ownerSessionId
241
+ ? { ownerSessionId: details.delivery.ownerSessionId }
242
+ : {}),
243
+ ...(details.delivery.ownerEpoch !== undefined
244
+ ? { ownerEpoch: details.delivery.ownerEpoch }
245
+ : {}),
246
+ state: details.delivery.state,
247
+ attempts: details.delivery.attempts,
248
+ updatedAt: details.delivery.updatedAt,
249
+ ...(details.delivery.deliveredAt !== undefined
250
+ ? { deliveredAt: details.delivery.deliveredAt }
251
+ : {}),
252
+ ...(details.delivery.lastError
253
+ ? {
254
+ lastError: bounded(
255
+ details.delivery.lastError,
256
+ MAX_ERROR_BYTES,
257
+ ),
258
+ }
259
+ : {}),
260
+ },
261
+ }
262
+ : {}),
263
+ ...(details.result !== undefined || details.resultArtifact
264
+ ? {
265
+ result: details.resultArtifact
266
+ ? "[stored in result.json]"
267
+ : "[result omitted from memory]",
268
+ ...(details.resultArtifact
269
+ ? { resultArtifact: details.resultArtifact }
270
+ : {}),
271
+ }
272
+ : {}),
273
+ ...(details.transcriptArtifact
274
+ ? { transcriptArtifact: details.transcriptArtifact }
275
+ : {}),
276
+ ...(details.resumedFrom ? { resumedFrom: details.resumedFrom } : {}),
277
+ ...(details.resumeNote
278
+ ? { resumeNote: bounded(details.resumeNote, MAX_ERROR_BYTES) }
279
+ : {}),
280
+ ...(details.error
281
+ ? { error: bounded(details.error, MAX_ERROR_BYTES) }
282
+ : {}),
283
+ ...(details.graph ? { graphOmitted: true } : {}),
284
+ };
285
+
286
+ const sourceBytes = jsonBytes(details);
287
+ const projectedBytes = jsonBytes(candidate);
288
+ const omittedBytes = Math.max(0, sourceBytes - projectedBytes);
289
+ const metadata = (bytes: number): WorkflowMemoryProjection => ({
290
+ kind: "settled",
291
+ maxBytes: cap,
292
+ bytes,
293
+ truncated:
294
+ omittedBytes > 0 ||
295
+ omitted.agents > 0 ||
296
+ omitted.logs > 0 ||
297
+ omitted.transcriptEntries > 0 ||
298
+ omitted.result ||
299
+ omitted.graph,
300
+ omitted,
301
+ });
302
+
303
+ return { candidate, metadata };
304
+ }
305
+
306
+ /**
307
+ * Build a detached settled-run projection. It intentionally never copies
308
+ * transcripts or arbitrary result values; those remain in the run artifacts.
309
+ */
310
+ export function projectWorkflowDetails(
311
+ details: WorkflowDetails,
312
+ maxBytes = DEFAULT_WORKFLOW_SETTLED_MAX_BYTES,
313
+ ): WorkflowDetails | undefined {
314
+ const cap = validateLimit(maxBytes, "maxBytes");
315
+ if (cap === 0) return undefined;
316
+
317
+ let display = true;
318
+ let agentLimit = details.agents.length;
319
+ let logLimit = Math.min(MAX_LOGS, details.logs?.length ?? 0);
320
+ let agents = details.agents.map((agent) => compactAgent(agent, display));
321
+ let logs = (details.logs ?? []).slice(-logLimit).map((log) => ({
322
+ at: log.at,
323
+ text: bounded(log.text, MAX_PREVIEW_BYTES) ?? "",
324
+ }));
325
+
326
+ const removeOptionalFields = () => {
327
+ display = false;
328
+ agents = details.agents.map((agent) => compactAgent(agent, false));
329
+ logs = [];
330
+ logLimit = 0;
331
+ };
332
+
333
+ const dropAgent = () => {
334
+ if (agents.length === 0) return false;
335
+ // Keep the oldest and newest identities preferentially while pressure
336
+ // removes middle display rows. Lifecycle facts for retained rows survive.
337
+ const index = agents.length > 1 ? Math.floor(agents.length / 2) : 0;
338
+ agents = [...agents.slice(0, index), ...agents.slice(index + 1)];
339
+ agentLimit--;
340
+ return true;
341
+ };
342
+
343
+ const dropRootOptional = (candidate: Record<string, unknown>) => {
344
+ for (const key of [
345
+ "description",
346
+ "resumeNote",
347
+ "error",
348
+ "currentPhase",
349
+ "name",
350
+ "sessionId",
351
+ "logsDropped",
352
+ "logs",
353
+ "graphOmitted",
354
+ "result",
355
+ ]) {
356
+ if (key in candidate) {
357
+ delete candidate[key];
358
+ return true;
359
+ }
360
+ }
361
+ return false;
362
+ };
363
+
364
+ let candidate: Record<string, unknown> | undefined;
365
+ let projection: WorkflowMemoryProjection | undefined;
366
+ for (let pass = 0; pass < details.agents.length + 32; pass++) {
367
+ const built = makeProjection(
368
+ details,
369
+ cap,
370
+ agents,
371
+ logs,
372
+ agentLimit,
373
+ logLimit,
374
+ );
375
+ candidate = built.candidate;
376
+ projection = built.metadata(0);
377
+ candidate.memoryProjection = projection;
378
+ let bytes = jsonBytes(candidate);
379
+ if (bytes <= cap) {
380
+ // The byte count is itself part of the projection. Iterate until the
381
+ // decimal width of that field stabilizes.
382
+ for (let i = 0; i < 4; i++) {
383
+ candidate.memoryProjection = built.metadata(bytes);
384
+ const next = jsonBytes(candidate);
385
+ if (next === bytes) break;
386
+ bytes = next;
387
+ }
388
+ candidate.memoryProjection = built.metadata(bytes);
389
+ bytes = jsonBytes(candidate);
390
+ if (bytes <= cap) return candidate as unknown as WorkflowDetails;
391
+ }
392
+
393
+ if (display) {
394
+ removeOptionalFields();
395
+ continue;
396
+ }
397
+ if (dropAgent()) continue;
398
+ if (dropRootOptional(candidate)) continue;
399
+
400
+ // The exact run id and terminal identity are non-negotiable. A caller
401
+ // using an impossibly small test budget gets an explicit non-retained run
402
+ // rather than a projection that cannot be addressed or measured safely.
403
+ const minimal: Record<string, unknown> = {
404
+ runId: details.runId,
405
+ background: details.background,
406
+ status: details.status,
407
+ startedAt: details.startedAt,
408
+ ...(details.finishedAt !== undefined
409
+ ? { finishedAt: details.finishedAt }
410
+ : {}),
411
+ phases: [],
412
+ agents: [],
413
+ ...(details.resultArtifact
414
+ ? { resultArtifact: details.resultArtifact }
415
+ : {}),
416
+ ...(details.transcriptArtifact
417
+ ? { transcriptArtifact: details.transcriptArtifact }
418
+ : {}),
419
+ };
420
+ const minimalProjection = makeProjection(
421
+ details,
422
+ cap,
423
+ [],
424
+ [],
425
+ 0,
426
+ 0,
427
+ ).metadata(0);
428
+ minimal.memoryProjection = minimalProjection;
429
+ const minimalBytes = jsonBytes(minimal);
430
+ if (minimalBytes <= cap) {
431
+ minimal.memoryProjection = { ...minimalProjection, bytes: minimalBytes };
432
+ if (jsonBytes(minimal) <= cap)
433
+ return minimal as unknown as WorkflowDetails;
434
+ }
435
+ return undefined;
436
+ }
437
+ return undefined;
438
+ }
439
+
440
+ export function measureWorkflowDetailsBytes(details: WorkflowDetails) {
441
+ return jsonBytes(details);
442
+ }
443
+
444
+ interface RetainedEntry {
445
+ readonly details: WorkflowDetails;
446
+ readonly bytes: number;
447
+ }
448
+
449
+ /** Count- and byte-bounded insertion-ordered store for settled projections. */
450
+ export class WorkflowSettledRunRetention {
451
+ readonly maxRuns: number;
452
+ readonly maxBytes: number;
453
+ private readonly entries = new Map<string, RetainedEntry>();
454
+ private totalBytes = 0;
455
+ private totalEvictedRuns = 0;
456
+ private totalEvictedBytes = 0;
457
+
458
+ constructor(options: WorkflowSettledRunRetentionOptions = {}) {
459
+ this.maxRuns = validateLimit(
460
+ options.maxRuns ?? DEFAULT_WORKFLOW_SETTLED_MAX_RUNS,
461
+ "maxRuns",
462
+ );
463
+ this.maxBytes = validateLimit(
464
+ options.maxBytes ?? DEFAULT_WORKFLOW_SETTLED_MAX_BYTES,
465
+ "maxBytes",
466
+ );
467
+ }
468
+
469
+ set(details: WorkflowDetails) {
470
+ const previous = this.entries.get(details.runId);
471
+ if (previous) {
472
+ this.entries.delete(details.runId);
473
+ this.totalBytes -= previous.bytes;
474
+ }
475
+ const projection = projectWorkflowDetails(details, this.maxBytes);
476
+ if (!projection) {
477
+ // Keep a previously valid projection addressable when an unusually small
478
+ // configured budget cannot represent a later update. The canonical file
479
+ // remains the recovery source either way.
480
+ if (previous) {
481
+ this.entries.set(details.runId, previous);
482
+ this.totalBytes += previous.bytes;
483
+ } else {
484
+ this.totalEvictedRuns++;
485
+ }
486
+ return undefined;
487
+ }
488
+ const bytes = measureWorkflowDetailsBytes(projection);
489
+ this.entries.set(details.runId, { details: projection, bytes });
490
+ this.totalBytes += bytes;
491
+ while (
492
+ this.entries.size > this.maxRuns ||
493
+ this.totalBytes > this.maxBytes
494
+ ) {
495
+ const oldest = this.entries.keys().next().value;
496
+ if (oldest === undefined) break;
497
+ const entry = this.entries.get(oldest);
498
+ this.entries.delete(oldest);
499
+ if (!entry) continue;
500
+ this.totalBytes -= entry.bytes;
501
+ this.totalEvictedRuns++;
502
+ this.totalEvictedBytes += entry.bytes;
503
+ }
504
+ return this.entries.get(details.runId)?.details;
505
+ }
506
+
507
+ get(runId: string) {
508
+ return this.entries.get(runId)?.details;
509
+ }
510
+
511
+ has(runId: string) {
512
+ return this.entries.has(runId);
513
+ }
514
+
515
+ delete(runId: string) {
516
+ const entry = this.entries.get(runId);
517
+ if (!entry) return false;
518
+ this.entries.delete(runId);
519
+ this.totalBytes -= entry.bytes;
520
+ return true;
521
+ }
522
+
523
+ clear() {
524
+ this.entries.clear();
525
+ this.totalBytes = 0;
526
+ }
527
+
528
+ /** Clear retained projections without changing current-session counters. */
529
+ reset() {
530
+ this.clear();
531
+ }
532
+
533
+ /** Start a new session accounting epoch. */
534
+ resetStats() {
535
+ this.totalEvictedRuns = 0;
536
+ this.totalEvictedBytes = 0;
537
+ }
538
+
539
+ /** Clear projections and start a new session accounting epoch. */
540
+ resetSession() {
541
+ this.clear();
542
+ this.resetStats();
543
+ }
544
+
545
+ keys() {
546
+ return this.entries.keys();
547
+ }
548
+
549
+ values() {
550
+ return [...this.entries.values()].map((entry) => entry.details).values();
551
+ }
552
+
553
+ entriesArray() {
554
+ return [...this.entries.entries()].map(
555
+ ([id, entry]) => [id, entry.details] as const,
556
+ );
557
+ }
558
+
559
+ get size() {
560
+ return this.entries.size;
561
+ }
562
+
563
+ get retainedBytes() {
564
+ return this.totalBytes;
565
+ }
566
+
567
+ get evictedRuns() {
568
+ return this.totalEvictedRuns;
569
+ }
570
+
571
+ get settledRunsEvicted() {
572
+ return this.totalEvictedRuns;
573
+ }
574
+
575
+ get evictedBytes() {
576
+ return this.totalEvictedBytes;
577
+ }
578
+
579
+ get stats(): WorkflowRetentionStats {
580
+ return {
581
+ scope: "current-session",
582
+ retainedRuns: this.entries.size,
583
+ retainedBytes: this.totalBytes,
584
+ evictedRuns: this.totalEvictedRuns,
585
+ settledRunsEvicted: this.totalEvictedRuns,
586
+ evictedBytes: this.totalEvictedBytes,
587
+ };
588
+ }
589
+
590
+ getStats() {
591
+ return this.stats;
592
+ }
593
+ }
594
+
595
+ export function createWorkflowSettledRunRetention(
596
+ options: WorkflowSettledRunRetentionOptions = {},
597
+ ) {
598
+ return new WorkflowSettledRunRetention(options);
599
+ }