@tt-a1i/openpi 0.1.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 (114) hide show
  1. package/README.md +643 -0
  2. package/SETUP.md +74 -0
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-package.png +0 -0
  5. package/assets/readme-hero-mobile.svg +72 -0
  6. package/assets/readme-hero.svg +118 -0
  7. package/assets/readme-runtime-mobile.svg +91 -0
  8. package/assets/readme-runtime.svg +111 -0
  9. package/extensions/ask-user/handoff.ts +205 -0
  10. package/extensions/ask-user/index.ts +1110 -0
  11. package/extensions/ask-user/limits.ts +89 -0
  12. package/extensions/ask-user/prompt.ts +76 -0
  13. package/extensions/background-terminals/index.ts +653 -0
  14. package/extensions/background-terminals/src/domain.ts +99 -0
  15. package/extensions/background-terminals/src/manager.ts +989 -0
  16. package/extensions/background-terminals/src/output.ts +84 -0
  17. package/extensions/background-terminals/src/prompt.ts +195 -0
  18. package/extensions/background-terminals/src/result-delivery.ts +43 -0
  19. package/extensions/background-terminals/src/runtime.ts +36 -0
  20. package/extensions/background-terminals/src/ui/output-view.ts +55 -0
  21. package/extensions/background-terminals/src/ui/ps.ts +642 -0
  22. package/extensions/background-terminals/src/ui/tool-result.ts +146 -0
  23. package/extensions/background-terminals/src/watch.ts +192 -0
  24. package/extensions/context-pivot/index.ts +222 -0
  25. package/extensions/copy-all/index.ts +65 -0
  26. package/extensions/cron/index.ts +173 -0
  27. package/extensions/cron/schedule.ts +127 -0
  28. package/extensions/file-mutation-display/index.ts +105 -0
  29. package/extensions/file-mutation-display/render.ts +107 -0
  30. package/extensions/file-search/index.ts +515 -0
  31. package/extensions/file-search/src/args.ts +129 -0
  32. package/extensions/file-search/src/binaries.ts +419 -0
  33. package/extensions/file-search/src/output.ts +142 -0
  34. package/extensions/file-search/src/process.ts +309 -0
  35. package/extensions/file-search/src/prompt.ts +53 -0
  36. package/extensions/git-info/index.ts +272 -0
  37. package/extensions/git-info/src/changed-files-view.ts +414 -0
  38. package/extensions/git-info/src/process.ts +107 -0
  39. package/extensions/git-info/src/refresh-coordinator.ts +13 -0
  40. package/extensions/git-info/src/runtime.ts +28 -0
  41. package/extensions/goal/controller.ts +794 -0
  42. package/extensions/goal/index.ts +521 -0
  43. package/extensions/goal/prompts.ts +122 -0
  44. package/extensions/goal/state.ts +763 -0
  45. package/extensions/goal/ui.ts +158 -0
  46. package/extensions/model-info/index.ts +234 -0
  47. package/extensions/plan-mode/bash-policy.ts +313 -0
  48. package/extensions/plan-mode/index.ts +539 -0
  49. package/extensions/post-edit/index.ts +129 -0
  50. package/extensions/sessions/LICENSE.upstream +21 -0
  51. package/extensions/sessions/git-stats.ts +226 -0
  52. package/extensions/sessions/index.ts +1092 -0
  53. package/extensions/sessions/sessions.ts +385 -0
  54. package/extensions/setup/index.ts +408 -0
  55. package/extensions/shared/activity-status.ts +65 -0
  56. package/extensions/shared/below-editor-navigation.ts +343 -0
  57. package/extensions/shared/child-session.ts +352 -0
  58. package/extensions/shared/context-utilization.ts +47 -0
  59. package/extensions/shared/dashboard-state.ts +102 -0
  60. package/extensions/shared/plan-mode-state.ts +65 -0
  61. package/extensions/shared/setup-config.ts +971 -0
  62. package/extensions/shared/subagent-roles.ts +22 -0
  63. package/extensions/shared/terminal-text.ts +38 -0
  64. package/extensions/shared/tool-call-timeout.ts +104 -0
  65. package/extensions/shared/worktree.ts +526 -0
  66. package/extensions/subagents/index.ts +1225 -0
  67. package/extensions/subagents/navigation.ts +121 -0
  68. package/extensions/subagents/src/agent-types.ts +543 -0
  69. package/extensions/subagents/src/backend.ts +63 -0
  70. package/extensions/subagents/src/backends/pi.ts +493 -0
  71. package/extensions/subagents/src/backends/stub.ts +296 -0
  72. package/extensions/subagents/src/by-the-way.ts +21 -0
  73. package/extensions/subagents/src/domain.ts +271 -0
  74. package/extensions/subagents/src/format.ts +48 -0
  75. package/extensions/subagents/src/manager.ts +769 -0
  76. package/extensions/subagents/src/prompt.ts +190 -0
  77. package/extensions/subagents/src/result-delivery.ts +20 -0
  78. package/extensions/subagents/src/runtime.ts +51 -0
  79. package/extensions/subagents/src/ui/takeover.ts +615 -0
  80. package/extensions/subagents/src/ui/transcript.ts +293 -0
  81. package/extensions/subagents/src/ui/wait-result.ts +89 -0
  82. package/extensions/suggestions/index.ts +172 -0
  83. package/extensions/suggestions/src/config.ts +12 -0
  84. package/extensions/suggestions/src/predictor.ts +147 -0
  85. package/extensions/suggestions/src/prompt.ts +20 -0
  86. package/extensions/suggestions/src/transcript.ts +233 -0
  87. package/extensions/suggestions/src/ui.ts +224 -0
  88. package/extensions/tasks/index.ts +512 -0
  89. package/extensions/tasks/tasks.ts +649 -0
  90. package/extensions/tasks/ui.ts +421 -0
  91. package/extensions/turn-time/index.ts +61 -0
  92. package/extensions/ui-customization/footer.ts +512 -0
  93. package/extensions/ui-customization/index.ts +217 -0
  94. package/extensions/workflows/acceptance.ts +298 -0
  95. package/extensions/workflows/artifacts.ts +225 -0
  96. package/extensions/workflows/controller.ts +210 -0
  97. package/extensions/workflows/dashboard.ts +1226 -0
  98. package/extensions/workflows/index.ts +1884 -0
  99. package/extensions/workflows/journal.ts +188 -0
  100. package/extensions/workflows/meta.ts +250 -0
  101. package/extensions/workflows/model.ts +423 -0
  102. package/extensions/workflows/navigation.ts +93 -0
  103. package/extensions/workflows/prompt.ts +212 -0
  104. package/extensions/workflows/replay-safety.ts +577 -0
  105. package/extensions/workflows/runner.ts +786 -0
  106. package/extensions/workflows/sandbox-child.cjs +402 -0
  107. package/extensions/workflows/sandbox.ts +397 -0
  108. package/extensions/workflows/serialization.ts +162 -0
  109. package/extensions/workflows/worktree-handoff.ts +216 -0
  110. package/package.json +87 -0
  111. package/scripts/prepare-effect-tsgo.mjs +16 -0
  112. package/skills/background-terminals/SKILL.md +30 -0
  113. package/skills/subagents/SKILL.md +15 -0
  114. package/themes/github-dark-default.json +89 -0
@@ -0,0 +1,794 @@
1
+ import type { ImageContent } from "@earendil-works/pi-ai";
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { sanitizeTerminalText } from "../shared/terminal-text.ts";
7
+ import {
8
+ budgetLimitPrompt,
9
+ continuationPrompt,
10
+ objectiveUpdatedPrompt,
11
+ } from "./prompts.ts";
12
+ import {
13
+ GOAL_ENTRY_TYPE,
14
+ GoalRestoreError,
15
+ acknowledgeGoalCompletion,
16
+ budgetLimitTransition,
17
+ canResumeGoal,
18
+ clearBlockedAudit,
19
+ editGoalObjective,
20
+ emergencyLimitTransition,
21
+ isGoalActive,
22
+ isGoalVisible,
23
+ markContinuationDispatched,
24
+ normalizeGoalReason,
25
+ recordBlockedAudit,
26
+ recordGoalProgress,
27
+ restoreGoalState,
28
+ resumeGoal,
29
+ setContinuationDeferred,
30
+ transitionGoal,
31
+ validateGoalSnapshot,
32
+ type GoalSnapshot,
33
+ } from "./state.ts";
34
+
35
+ export const GOAL_CONTINUATION_TYPE = "goal-continuation";
36
+
37
+ type AssistantStopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
38
+
39
+ const BLOCKED_GOAL_TURN_MINIMUM = 3;
40
+
41
+ export interface GoalControllerOptions {
42
+ now?: () => number;
43
+ createId?: () => string;
44
+ }
45
+
46
+ export interface GoalModelUpdate {
47
+ goal: GoalSnapshot;
48
+ message: string;
49
+ blockedAudit?: {
50
+ blocker: string;
51
+ consecutiveTurns: number;
52
+ requiredTurns: number;
53
+ accepted: boolean;
54
+ };
55
+ }
56
+
57
+ export function countAssistantTokens(messages: readonly unknown[]) {
58
+ let total = 0;
59
+ for (const item of messages) {
60
+ const message = unwrapMessage(item);
61
+ if (!message || message.role !== "assistant") continue;
62
+ const tokens = assistantGoalTokens(message);
63
+ total += tokens;
64
+ if (!Number.isSafeInteger(total)) return 0;
65
+ }
66
+ return total;
67
+ }
68
+
69
+ export function lastAssistantStopReason(messages: readonly unknown[]) {
70
+ return lastAssistantMessage(messages)?.stopReason as
71
+ AssistantStopReason | undefined;
72
+ }
73
+
74
+ export function isUsageLimitError(messages: readonly unknown[]) {
75
+ const message = lastAssistantMessage(messages);
76
+ if (!message || message.stopReason !== "error") return false;
77
+ const diagnosticText = Array.isArray(message.diagnostics)
78
+ ? message.diagnostics
79
+ .flatMap((diagnostic) =>
80
+ isRecord(diagnostic) && isRecord(diagnostic.error)
81
+ ? [diagnostic.error.message, diagnostic.error.code]
82
+ : [],
83
+ )
84
+ .join(" ")
85
+ : "";
86
+ const text = `${String(message.errorMessage ?? "")} ${diagnosticText}`;
87
+ return /usage(?:_|\s+)limit|insufficient_quota|quota (?:has been )?exceeded|billing limit/iu.test(
88
+ text,
89
+ );
90
+ }
91
+
92
+ export class GoalController {
93
+ private readonly pi: ExtensionAPI;
94
+ private goal: GoalSnapshot | undefined;
95
+ private lockedReason: string | undefined;
96
+ private automationEnabled = false;
97
+ private continuationPending = false;
98
+ private completionAcknowledgementPending = false;
99
+ private readonly completionInputMarkers = new Set<string>();
100
+ private trackedGoalId: string | undefined;
101
+ private trackedStartedAt: number | undefined;
102
+ private trackedTokens = 0;
103
+ private currentRunObservedTokens = 0;
104
+ private currentRunFlushedTokens = 0;
105
+ private trackedFromRunStart = false;
106
+ private accountingGoalId: string | undefined;
107
+ private elapsedRemainderMs = 0;
108
+ private lastStopReason: AssistantStopReason | undefined;
109
+ private lastErrorWasUsageLimit = false;
110
+ private suppressAbortedStop = false;
111
+ private readonly now: () => number;
112
+ readonly createId: () => string;
113
+
114
+ constructor(pi: ExtensionAPI, options: GoalControllerOptions = {}) {
115
+ this.pi = pi;
116
+ this.now = options.now ?? Date.now;
117
+ this.createId = options.createId ?? (() => crypto.randomUUID());
118
+ }
119
+
120
+ snapshot() {
121
+ return isGoalVisible(this.goal) ? structuredClone(this.goal) : undefined;
122
+ }
123
+
124
+ footerSnapshot() {
125
+ const current = this.snapshot();
126
+ if (
127
+ current?.status === "complete" &&
128
+ this.completionAcknowledgementPending
129
+ ) {
130
+ return { ...current, completionAcknowledged: true as const };
131
+ }
132
+ return current;
133
+ }
134
+
135
+ revision() {
136
+ return this.goal?.revision ?? 0;
137
+ }
138
+
139
+ problem() {
140
+ return this.lockedReason;
141
+ }
142
+
143
+ restore(ctx: ExtensionContext, deferActive = false) {
144
+ this.resetRuntime();
145
+ this.automationEnabled = ctx.mode !== "print" && ctx.mode !== "json";
146
+ try {
147
+ const restored = restoreGoalState(ctx.sessionManager.getBranch());
148
+ this.goal = restored.snapshot;
149
+ this.lockedReason = undefined;
150
+ if (restored.migrated && this.goal) this.persist(this.goal);
151
+ if (deferActive && this.goal?.status === "active") {
152
+ this.persist(setContinuationDeferred(this.goal, true, this.now()));
153
+ }
154
+ } catch (error) {
155
+ this.goal = undefined;
156
+ this.lockedReason = sanitizeTerminalText(
157
+ error instanceof GoalRestoreError || error instanceof Error
158
+ ? error.message
159
+ : String(error),
160
+ );
161
+ }
162
+ }
163
+
164
+ replace(candidate: GoalSnapshot) {
165
+ this.assertUnlocked();
166
+ this.continuationPending = false;
167
+ this.completionInputMarkers.clear();
168
+ this.completionAcknowledgementPending = false;
169
+ if (this.goal?.id !== candidate.id) this.resetAccountingClock();
170
+ this.persist(candidate);
171
+ return this.snapshot();
172
+ }
173
+
174
+ edit(objective: string, ctx: ExtensionContext) {
175
+ this.assertUnlocked();
176
+ const current = this.requireVisibleGoal();
177
+ this.flushTrackedProgress();
178
+ this.persist(
179
+ editGoalObjective(this.goal ?? current, objective, this.now()),
180
+ );
181
+ if (this.goal?.status === "active") {
182
+ this.continuationPending = false;
183
+ if (!ctx.isIdle()) this.beginTracking(this.goal);
184
+ this.dispatchPrompt(
185
+ ctx,
186
+ ctx.isIdle() ? "continuation" : "objective_updated",
187
+ true,
188
+ );
189
+ }
190
+ return this.snapshot();
191
+ }
192
+
193
+ pause(reason = "Paused by user.") {
194
+ this.assertUnlocked();
195
+ const current = this.requireVisibleGoal();
196
+ if (current.status === "paused") return this.snapshot();
197
+ if (current.status !== "active") {
198
+ throw new Error(`Goal cannot pause from ${current.status}.`);
199
+ }
200
+ this.flushTrackedProgress();
201
+ this.continuationPending = false;
202
+ this.persist(
203
+ transitionGoal(this.goal ?? current, "paused", this.now(), reason),
204
+ );
205
+ this.resetAccountingClock();
206
+ return this.snapshot();
207
+ }
208
+
209
+ resume() {
210
+ this.assertUnlocked();
211
+ const current = this.requireVisibleGoal();
212
+ if (!canResumeGoal(current)) {
213
+ throw new Error(`Goal cannot resume from ${current.status}.`);
214
+ }
215
+ this.continuationPending = false;
216
+ this.resetAccountingClock();
217
+ this.persist(resumeGoal(current, this.now(), "Resumed by user."));
218
+ return this.snapshot();
219
+ }
220
+
221
+ updateFromModel(status: "complete" | "blocked", blocker?: unknown) {
222
+ this.assertUnlocked();
223
+ const current = this.requireVisibleGoal();
224
+ if (status === "blocked" && blocker === undefined) {
225
+ throw new Error("Blocked goal updates require a blocker description.");
226
+ }
227
+ const normalizedBlocker =
228
+ status === "blocked" ? normalizeGoalReason(blocker) : undefined;
229
+ if (current.status === status) {
230
+ return this.modelUpdate(current, `Goal is already ${status}.`);
231
+ }
232
+ if (current.status === "budget_limited" && status === "blocked") {
233
+ return this.modelUpdate(
234
+ current,
235
+ "Goal remains limited by budget; blocked cannot replace that status.",
236
+ );
237
+ }
238
+ if (status === "blocked") {
239
+ const recorded = recordBlockedAudit(
240
+ current,
241
+ normalizedBlocker,
242
+ this.now(),
243
+ );
244
+ if (recorded.recorded) this.persist(recorded.snapshot);
245
+ if (recorded.audit.consecutiveTurns < BLOCKED_GOAL_TURN_MINIMUM) {
246
+ return this.modelUpdate(
247
+ this.goal ?? recorded.snapshot,
248
+ `Blocker recorded for goal turn ${recorded.audit.consecutiveTurns} of ${BLOCKED_GOAL_TURN_MINIMUM}; the goal remains active. Continue working until the same blocker recurs on ${BLOCKED_GOAL_TURN_MINIMUM} consecutive distinct goal turns.`,
249
+ {
250
+ blocker: recorded.audit.blocker,
251
+ consecutiveTurns: recorded.audit.consecutiveTurns,
252
+ requiredTurns: BLOCKED_GOAL_TURN_MINIMUM,
253
+ accepted: false,
254
+ },
255
+ );
256
+ }
257
+ }
258
+ this.flushTrackedProgress();
259
+ this.continuationPending = false;
260
+ this.persist(
261
+ transitionGoal(
262
+ this.goal ?? current,
263
+ status,
264
+ this.now(),
265
+ status === "complete"
266
+ ? "Marked complete by the goal agent."
267
+ : normalizedBlocker,
268
+ ),
269
+ );
270
+ this.resetAccountingClock();
271
+ return this.modelUpdate(
272
+ this.goal!,
273
+ status === "complete" ? "Goal marked complete." : "Goal marked blocked.",
274
+ status === "blocked"
275
+ ? {
276
+ blocker: normalizedBlocker!,
277
+ consecutiveTurns: BLOCKED_GOAL_TURN_MINIMUM,
278
+ requiredTurns: BLOCKED_GOAL_TURN_MINIMUM,
279
+ accepted: true,
280
+ }
281
+ : undefined,
282
+ );
283
+ }
284
+
285
+ clear() {
286
+ this.assertUnlocked();
287
+ if (!isGoalVisible(this.goal)) return false;
288
+ this.flushTrackedProgress();
289
+ this.continuationPending = false;
290
+ this.persist(
291
+ transitionGoal(this.goal, "cleared", this.now(), "Cleared by user."),
292
+ );
293
+ this.resetAccountingClock();
294
+ return true;
295
+ }
296
+
297
+ kickoff(ctx: ExtensionContext) {
298
+ this.assertUnlocked();
299
+ const current = this.requireVisibleGoal();
300
+ if (current.status !== "active") {
301
+ throw new Error(`Goal cannot start from ${current.status}.`);
302
+ }
303
+ if (!ctx.isIdle()) this.beginTracking(current);
304
+ return this.dispatchPrompt(ctx, "continuation", true);
305
+ }
306
+
307
+ continueWhenIdle(ctx: ExtensionContext) {
308
+ return this.dispatchPrompt(ctx, "continuation", false);
309
+ }
310
+
311
+ sanitizeCompletionMarkerImages(images: readonly ImageContent[] = []) {
312
+ let changed = false;
313
+ const sanitized = images.filter((image) => {
314
+ if (image.mimeType !== GOAL_COMPLETION_MARKER_MIME) return true;
315
+ changed = true;
316
+ this.completionInputMarkers.delete(image.data);
317
+ return false;
318
+ });
319
+ return { images: sanitized, changed };
320
+ }
321
+
322
+ prepareExplicitInput() {
323
+ let marker: ImageContent | undefined;
324
+ if (this.goal?.status === "complete" && !this.goal.completionAcknowledged) {
325
+ const data = crypto.randomUUID();
326
+ if (this.completionInputMarkers.size >= 32) {
327
+ const oldest = this.completionInputMarkers.values().next().value;
328
+ if (oldest !== undefined) this.completionInputMarkers.delete(oldest);
329
+ }
330
+ this.completionInputMarkers.add(data);
331
+ marker = {
332
+ type: "image",
333
+ data,
334
+ mimeType: GOAL_COMPLETION_MARKER_MIME,
335
+ };
336
+ }
337
+ if (this.goal?.status === "active" && this.goal.deferContinuation) {
338
+ this.persist(setContinuationDeferred(this.goal, false, this.now()));
339
+ }
340
+ return marker;
341
+ }
342
+
343
+ releaseDeferredContinuation() {
344
+ if (this.goal?.status !== "active" || !this.goal.deferContinuation) {
345
+ return false;
346
+ }
347
+ this.persist(setContinuationDeferred(this.goal, false, this.now()));
348
+ return true;
349
+ }
350
+
351
+ agentStarted() {
352
+ this.continuationPending = false;
353
+ this.lastStopReason = undefined;
354
+ this.lastErrorWasUsageLimit = false;
355
+ this.suppressAbortedStop = false;
356
+ this.currentRunObservedTokens = 0;
357
+ this.currentRunFlushedTokens = 0;
358
+ if (!this.goal || !isGoalActive(this.goal) || this.goal.deferContinuation) {
359
+ if (!this.trackedGoalId) this.resetTrackedRun();
360
+ else this.trackedFromRunStart = true;
361
+ return;
362
+ }
363
+ this.beginTracking(this.goal, true);
364
+ }
365
+
366
+ messageEnded(message: unknown) {
367
+ const unwrapped = unwrapMessage(message);
368
+ if (!unwrapped) return undefined;
369
+ if (unwrapped.role === "user") {
370
+ const sanitized = stripKnownMarkers(
371
+ unwrapped,
372
+ this.completionInputMarkers,
373
+ );
374
+ if (!sanitized) return undefined;
375
+ if (sanitized.matchedTrackedMarker) {
376
+ this.completionAcknowledgementPending = true;
377
+ }
378
+ return {
379
+ message: sanitized.message,
380
+ footerChanged: sanitized.matchedTrackedMarker,
381
+ };
382
+ }
383
+ if (unwrapped.role !== "assistant") return undefined;
384
+ this.currentRunObservedTokens = safeTokenAdd(
385
+ this.currentRunObservedTokens,
386
+ assistantGoalTokens(unwrapped),
387
+ );
388
+ return undefined;
389
+ }
390
+
391
+ turnEnded() {
392
+ this.persistPendingCompletionAcknowledgement();
393
+ }
394
+
395
+ settledWithoutAcknowledgement() {
396
+ this.completionInputMarkers.clear();
397
+ }
398
+
399
+ agentEnded(messages: readonly unknown[]) {
400
+ if (!this.trackedGoalId) return;
401
+ const observed = this.trackedFromRunStart
402
+ ? Math.max(this.currentRunObservedTokens, countAssistantTokens(messages))
403
+ : this.currentRunObservedTokens;
404
+ this.trackedTokens = safeTokenAdd(
405
+ this.trackedTokens,
406
+ Math.max(0, observed - this.currentRunFlushedTokens),
407
+ );
408
+ this.currentRunObservedTokens = 0;
409
+ this.currentRunFlushedTokens = 0;
410
+ this.lastStopReason = lastAssistantStopReason(messages);
411
+ this.lastErrorWasUsageLimit = isUsageLimitError(messages);
412
+ }
413
+
414
+ toolFinished(ctx: ExtensionContext) {
415
+ if (
416
+ !this.trackedGoalId ||
417
+ !this.goal ||
418
+ this.goal.id !== this.trackedGoalId ||
419
+ this.goal.status !== "active"
420
+ ) {
421
+ return;
422
+ }
423
+ this.flushTrackedProgress();
424
+ if (!this.goal || this.goal.status !== "active") return;
425
+ const budgetLimited = budgetLimitTransition(this.goal, this.now());
426
+ if (!budgetLimited) return;
427
+ this.persist(budgetLimited);
428
+ this.resetAccountingClock();
429
+ this.dispatchPrompt(ctx, "budget_limit", true);
430
+ }
431
+
432
+ compacted(willRetry: boolean) {
433
+ if (willRetry && this.lastStopReason === "aborted") {
434
+ this.suppressAbortedStop = true;
435
+ }
436
+ }
437
+
438
+ settled(ctx: ExtensionContext) {
439
+ const trackedGoalId = this.trackedGoalId;
440
+ if (trackedGoalId) this.continuationPending = false;
441
+ const stopReason = this.lastStopReason;
442
+ const errorWasUsageLimit = this.lastErrorWasUsageLimit;
443
+ const suppressAbortedStop = this.suppressAbortedStop;
444
+
445
+ if (!trackedGoalId || !this.goal || this.goal.id !== trackedGoalId) {
446
+ this.resetTrackedRun();
447
+ return;
448
+ }
449
+
450
+ this.flushTrackedProgress();
451
+ this.resetTrackedRun();
452
+ if (!this.goal || this.goal.id !== trackedGoalId) return;
453
+ if (
454
+ this.goal.status === "budget_limited" &&
455
+ stopReason === "error" &&
456
+ errorWasUsageLimit
457
+ ) {
458
+ this.persist(
459
+ transitionGoal(
460
+ this.goal,
461
+ "usage_limited",
462
+ this.now(),
463
+ "Stopped after the active turn hit a usage limit.",
464
+ ),
465
+ );
466
+ this.resetAccountingClock();
467
+ return;
468
+ }
469
+ if (this.goal.status !== "active") return;
470
+
471
+ this.resetUnreportedBlockedAudit();
472
+ const budgetLimited = budgetLimitTransition(this.goal, this.now());
473
+ if (budgetLimited) {
474
+ this.persist(budgetLimited);
475
+ this.resetAccountingClock();
476
+ this.dispatchPrompt(ctx, "budget_limit", true);
477
+ return;
478
+ }
479
+ if (stopReason === "aborted" && !suppressAbortedStop) {
480
+ this.persist(
481
+ transitionGoal(
482
+ this.goal,
483
+ "paused",
484
+ this.now(),
485
+ "Paused after the active turn was interrupted.",
486
+ ),
487
+ );
488
+ this.resetAccountingClock();
489
+ return;
490
+ }
491
+ if (stopReason === "error") {
492
+ this.persist(
493
+ transitionGoal(
494
+ this.goal,
495
+ errorWasUsageLimit ? "usage_limited" : "blocked",
496
+ this.now(),
497
+ errorWasUsageLimit
498
+ ? "Stopped after the active turn hit a usage limit."
499
+ : "Blocked after the active turn ended with an error.",
500
+ ),
501
+ );
502
+ this.resetAccountingClock();
503
+ return;
504
+ }
505
+
506
+ const emergencyLimited = emergencyLimitTransition(this.goal, this.now());
507
+ if (emergencyLimited) {
508
+ this.persist(emergencyLimited);
509
+ this.resetAccountingClock();
510
+ return;
511
+ }
512
+ this.continueWhenIdle(ctx);
513
+ }
514
+
515
+ shutdown() {
516
+ this.automationEnabled = false;
517
+ this.resetRuntime();
518
+ }
519
+
520
+ private modelUpdate(
521
+ goal: GoalSnapshot,
522
+ message: string,
523
+ blockedAudit?: GoalModelUpdate["blockedAudit"],
524
+ ) {
525
+ return {
526
+ goal: structuredClone(goal),
527
+ message,
528
+ ...(blockedAudit === undefined ? {} : { blockedAudit }),
529
+ };
530
+ }
531
+
532
+ private resetUnreportedBlockedAudit() {
533
+ if (!this.goal?.blockedAudit) return;
534
+ if (
535
+ this.goal.blockedAudit.lastTurn === (this.goal.continuationCount ?? 0)
536
+ ) {
537
+ return;
538
+ }
539
+ this.persist(clearBlockedAudit(this.goal, this.now()));
540
+ }
541
+
542
+ private persistPendingCompletionAcknowledgement() {
543
+ if (
544
+ !this.completionAcknowledgementPending ||
545
+ this.goal?.status !== "complete" ||
546
+ this.goal.completionAcknowledged
547
+ ) {
548
+ this.completionAcknowledgementPending = false;
549
+ return;
550
+ }
551
+ this.persist(acknowledgeGoalCompletion(this.goal, this.now()));
552
+ this.completionAcknowledgementPending = false;
553
+ }
554
+
555
+ private dispatchPrompt(
556
+ ctx: ExtensionContext,
557
+ kind: "continuation" | "objective_updated" | "budget_limit",
558
+ allowBusy: boolean,
559
+ ) {
560
+ if (!this.automationEnabled || !this.goal || this.continuationPending) {
561
+ return false;
562
+ }
563
+ if (!allowBusy && !ctx.isIdle()) return false;
564
+ if (kind !== "budget_limit") {
565
+ if (this.goal.status !== "active" || this.goal.deferContinuation) {
566
+ return false;
567
+ }
568
+ const emergencyLimited = emergencyLimitTransition(this.goal, this.now());
569
+ if (emergencyLimited) {
570
+ this.persist(emergencyLimited);
571
+ return false;
572
+ }
573
+ } else if (this.goal.status !== "budget_limited") {
574
+ return false;
575
+ }
576
+
577
+ if (kind === "budget_limit") this.beginTracking(this.goal);
578
+ const goal = this.goal;
579
+ const content =
580
+ kind === "continuation"
581
+ ? continuationPrompt(goal)
582
+ : kind === "objective_updated"
583
+ ? objectiveUpdatedPrompt(goal)
584
+ : budgetLimitPrompt(goal);
585
+ this.continuationPending = true;
586
+ try {
587
+ this.pi.sendMessage(
588
+ {
589
+ customType: GOAL_CONTINUATION_TYPE,
590
+ display: true,
591
+ content,
592
+ details: { kind, goalId: goal.id, revision: goal.revision },
593
+ },
594
+ {
595
+ triggerTurn: true,
596
+ deliverAs: ctx.isIdle() ? "followUp" : "steer",
597
+ },
598
+ );
599
+ } catch (error) {
600
+ this.continuationPending = false;
601
+ if (this.goal?.status === "active") {
602
+ this.persist(
603
+ transitionGoal(
604
+ this.goal,
605
+ "paused",
606
+ this.now(),
607
+ "Paused because the next autonomous turn could not be dispatched.",
608
+ ),
609
+ );
610
+ }
611
+ throw error;
612
+ }
613
+ // Count the continuation only after the turn was actually dispatched, so a
614
+ // failed send never erodes the emergency safety budget with zero productive
615
+ // turns. The in-memory continuationPending guard (set above) is what blocks
616
+ // a concurrent dispatch in the window before this persist lands.
617
+ if (kind !== "budget_limit") {
618
+ this.persist(markContinuationDispatched(this.goal, this.now()));
619
+ }
620
+ return true;
621
+ }
622
+
623
+ private persist(candidate: GoalSnapshot) {
624
+ const checked = validateGoalSnapshot(candidate);
625
+ this.pi.appendEntry(GOAL_ENTRY_TYPE, checked);
626
+ this.goal = checked;
627
+ }
628
+
629
+ private beginTracking(goal: GoalSnapshot, fromRunStart = false) {
630
+ if (this.accountingGoalId !== goal.id) {
631
+ this.accountingGoalId = goal.id;
632
+ this.elapsedRemainderMs = 0;
633
+ }
634
+ if (this.trackedGoalId !== goal.id) {
635
+ this.trackedGoalId = goal.id;
636
+ this.trackedStartedAt = this.now();
637
+ this.trackedTokens = 0;
638
+ this.currentRunFlushedTokens = fromRunStart
639
+ ? 0
640
+ : this.currentRunObservedTokens;
641
+ this.trackedFromRunStart = fromRunStart;
642
+ } else if (fromRunStart) {
643
+ this.trackedFromRunStart = true;
644
+ }
645
+ }
646
+
647
+ private flushTrackedProgress() {
648
+ if (
649
+ !this.trackedGoalId ||
650
+ !this.goal ||
651
+ this.goal.id !== this.trackedGoalId ||
652
+ this.goal.status === "cleared"
653
+ ) {
654
+ return;
655
+ }
656
+ const unflushedCurrentTokens = Math.max(
657
+ 0,
658
+ this.currentRunObservedTokens - this.currentRunFlushedTokens,
659
+ );
660
+ const tokens = safeTokenAdd(this.trackedTokens, unflushedCurrentTokens);
661
+ const now = this.now();
662
+ const elapsedMs =
663
+ this.trackedStartedAt === undefined
664
+ ? 0
665
+ : Math.max(0, now - this.trackedStartedAt);
666
+ const totalElapsedMs = this.elapsedRemainderMs + elapsedMs;
667
+ const elapsedSeconds = Math.floor(totalElapsedMs / 1_000);
668
+ this.elapsedRemainderMs = totalElapsedMs % 1_000;
669
+ if (tokens > 0 || elapsedSeconds > 0) {
670
+ this.persist(recordGoalProgress(this.goal, tokens, elapsedSeconds, now));
671
+ }
672
+ this.trackedTokens = 0;
673
+ this.currentRunFlushedTokens = this.currentRunObservedTokens;
674
+ this.trackedStartedAt = now;
675
+ }
676
+
677
+ private requireVisibleGoal() {
678
+ if (!isGoalVisible(this.goal)) throw new Error("No goal is currently set.");
679
+ return this.goal;
680
+ }
681
+
682
+ private resetRuntime() {
683
+ this.continuationPending = false;
684
+ this.completionInputMarkers.clear();
685
+ this.completionAcknowledgementPending = false;
686
+ this.resetTrackedRun();
687
+ this.resetAccountingClock();
688
+ }
689
+
690
+ private resetTrackedRun() {
691
+ this.trackedGoalId = undefined;
692
+ this.trackedStartedAt = undefined;
693
+ this.trackedTokens = 0;
694
+ this.currentRunObservedTokens = 0;
695
+ this.currentRunFlushedTokens = 0;
696
+ this.trackedFromRunStart = false;
697
+ this.lastStopReason = undefined;
698
+ this.lastErrorWasUsageLimit = false;
699
+ this.suppressAbortedStop = false;
700
+ }
701
+
702
+ private resetAccountingClock() {
703
+ this.accountingGoalId = undefined;
704
+ this.elapsedRemainderMs = 0;
705
+ }
706
+
707
+ private assertUnlocked() {
708
+ if (this.lockedReason) {
709
+ throw new Error(
710
+ `Session goal is locked by malformed branch history: ${this.lockedReason}`,
711
+ );
712
+ }
713
+ }
714
+ }
715
+
716
+ function assistantGoalTokens(message: Record<string, unknown>) {
717
+ const usage = message.usage;
718
+ if (!isRecord(usage)) return 0;
719
+ const input = usage.input;
720
+ const output = usage.output;
721
+ if (
722
+ Number.isSafeInteger(input) &&
723
+ (input as number) >= 0 &&
724
+ Number.isSafeInteger(output) &&
725
+ (output as number) >= 0
726
+ ) {
727
+ return safeTokenAdd(input as number, output as number);
728
+ }
729
+ const total = usage.totalTokens;
730
+ return Number.isSafeInteger(total) && (total as number) >= 0
731
+ ? (total as number)
732
+ : 0;
733
+ }
734
+
735
+ function lastAssistantMessage(messages: readonly unknown[]) {
736
+ for (let index = messages.length - 1; index >= 0; index--) {
737
+ const message = unwrapMessage(messages[index]);
738
+ if (!message || message.role !== "assistant") continue;
739
+ if (
740
+ message.stopReason === "stop" ||
741
+ message.stopReason === "length" ||
742
+ message.stopReason === "toolUse" ||
743
+ message.stopReason === "error" ||
744
+ message.stopReason === "aborted"
745
+ ) {
746
+ return message;
747
+ }
748
+ }
749
+ return undefined;
750
+ }
751
+
752
+ function unwrapMessage(value: unknown) {
753
+ if (!isRecord(value)) return undefined;
754
+ if (value.role) return value;
755
+ return isRecord(value.message) ? value.message : undefined;
756
+ }
757
+
758
+ export const GOAL_COMPLETION_MARKER_MIME =
759
+ "application/x-pi-goal-completion-ack";
760
+
761
+ function stripKnownMarkers(
762
+ message: Record<string, unknown>,
763
+ markers: Set<string>,
764
+ ) {
765
+ if (!Array.isArray(message.content)) return undefined;
766
+ let matchedTrackedMarker = false;
767
+ let foundPrivateMarker = false;
768
+ const content = message.content.filter((part) => {
769
+ if (
770
+ !isRecord(part) ||
771
+ part.type !== "image" ||
772
+ part.mimeType !== GOAL_COMPLETION_MARKER_MIME
773
+ ) {
774
+ return true;
775
+ }
776
+ foundPrivateMarker = true;
777
+ if (typeof part.data === "string" && markers.delete(part.data)) {
778
+ matchedTrackedMarker = true;
779
+ }
780
+ return false;
781
+ });
782
+ return foundPrivateMarker
783
+ ? { message: { ...message, content }, matchedTrackedMarker }
784
+ : undefined;
785
+ }
786
+
787
+ function safeTokenAdd(left: number, right: number) {
788
+ const result = left + right;
789
+ return Number.isSafeInteger(result) && result >= 0 ? result : left;
790
+ }
791
+
792
+ function isRecord(value: unknown): value is Record<string, unknown> {
793
+ return typeof value === "object" && value !== null && !Array.isArray(value);
794
+ }