@kky42/pi-goal 1.0.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 (41) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -0
  4. package/package.json +73 -0
  5. package/src/commands.ts +107 -0
  6. package/src/continuation-scheduler.ts +174 -0
  7. package/src/format.ts +232 -0
  8. package/src/goal-accounting.ts +128 -0
  9. package/src/goal-persistence.ts +73 -0
  10. package/src/goal-runtime-agent-handlers.ts +51 -0
  11. package/src/goal-runtime-controller.ts +162 -0
  12. package/src/goal-runtime-event-handler-types.ts +166 -0
  13. package/src/goal-runtime-event-handlers.ts +31 -0
  14. package/src/goal-runtime-event-utils.ts +93 -0
  15. package/src/goal-runtime-events.ts +24 -0
  16. package/src/goal-runtime-input-context-handlers.ts +144 -0
  17. package/src/goal-runtime-session-handlers.ts +131 -0
  18. package/src/goal-runtime-state.ts +22 -0
  19. package/src/goal-runtime-status.ts +62 -0
  20. package/src/goal-runtime-turn-handlers.ts +66 -0
  21. package/src/goal-state-controller.ts +210 -0
  22. package/src/goal-transition-effects.ts +91 -0
  23. package/src/goal-transition.ts +396 -0
  24. package/src/index.ts +9 -0
  25. package/src/prompts.ts +170 -0
  26. package/src/queued-goal-messages.ts +166 -0
  27. package/src/queued-goal-work.ts +96 -0
  28. package/src/recovery-adapters.ts +66 -0
  29. package/src/recovery-machine.ts +196 -0
  30. package/src/recovery-phase.ts +95 -0
  31. package/src/recovery-runtime.ts +97 -0
  32. package/src/recovery.ts +151 -0
  33. package/src/runtime-config.ts +7 -0
  34. package/src/stale-queued-work-guard.ts +114 -0
  35. package/src/stale-queued-work-obligations.ts +291 -0
  36. package/src/stale-queued-work-reducer.ts +483 -0
  37. package/src/stale-queued-work-terminal-cleanup.ts +84 -0
  38. package/src/stale-queued-work-types.ts +81 -0
  39. package/src/state.ts +404 -0
  40. package/src/tools.ts +101 -0
  41. package/src/types.ts +60 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,112 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 1.0.0 - 2026-05-30
6
+
7
+ - Renames the public package to `@kky42/pi-goal` for npm and pi package installs.
8
+ - Aligns goal initialization, continuation, blocked handling, prompt cache behavior, and tool-facing goal output with the Codex-style goal contract.
9
+ - Refreshes the README around installation, usage, and the Pi-Goal vs. Codex Goal comparison for the public 1.0.0 release.
10
+
11
+ ## 0.1.15 - 2026-05-27
12
+
13
+ - Refactors the goal runtime monolith into focused modules for clearer lifecycle ownership, event handling, and continuation orchestration.
14
+ - Narrows runtime handler dependency interfaces so input/context, turn, agent, and session handlers only receive the lifecycle ports they use.
15
+ - Moves goal transition effect application into a focused effect module so transition planning stays centered on goal snapshots and persistence decisions.
16
+ - Reworks the stale queued-work reducer around per-lifecycle default transition tables and focused state reducers, keeping no-op handling centralized while preserving explicit exceptional transitions.
17
+ - Removes the queued provider-context rewrite type assertion by returning typed provider-context rewrite intersections and clarifies the message normalization boundary comments.
18
+ - Hardens stale queued-work cleanup across abort, delayed terminal events, and continuation boundaries so stale work is consumed without mutating replacement-goal accounting.
19
+ - Tightens runtime continuation scheduling, recovery sequencing, and persistence/accounting handoff behavior with expanded regression coverage around lifecycle edge cases.
20
+ - Updates the local pi development baseline to `@earendil-works/*` `0.76.0` and refreshes the npm lockfile.
21
+ - Aligns recovery retry classification with Pi 0.76.0 so terminal quota, billing, and provider-limit errors do not stay pending for host retries even when they include `429` wording.
22
+ - Validates the cutover with the existing typecheck/test suite plus package metadata and dry-run pack checks.
23
+
24
+ ## 0.1.14 - 2026-05-26
25
+
26
+ - Widens the package Node engine range to support Node 22.19.0 through Node 26.x.
27
+
28
+ ## 0.1.13 - 2026-05-26
29
+
30
+ - Bounds hidden goal continuation provider context by superseding older active-goal continuations with short bookkeeping markers, refreshing only the latest continuation, and using compact auto-continuation prompts after `/goal` start or resume.
31
+ - Stops provider-error continuation retry storms by skipping immediate hidden requeues on `stopReason: "error"`, auto-compacting on context-window overflow when available, using bounded backoff for transient failures, and pausing with a recoverable `/goal resume` path when recovery is exhausted.
32
+ - Makes goal lifecycle transitions terminal and idempotent: duplicate `update_goal complete` calls no longer append extra session entries, completed goals cannot be paused or resumed, and runtime/compaction skips unchanged goal snapshots.
33
+ - Coalesces runtime goal persistence so repeated tool completions and unchanged compaction snapshots do not append full goal entries on every event; live footer usage stays current, and turn boundaries, shutdown, budget crossings, and bounded long-run intervals flush pending accounting to session history.
34
+ - Allows `create_goal` to replace a completed goal and clarifies recovery via `/goal <objective>` or `/goal clear`.
35
+ - Surfaces failed goal tool calls as real pi tool errors by throwing from tool handlers.
36
+
37
+ ## 0.1.12 - 2026-05-23
38
+
39
+ - Updated the local pi development baseline to `@earendil-works/*` `0.75.5`, refreshed Node/tsx tooling, and regenerated the npm lockfile.
40
+ - Reviewed the pi `0.75.5` changelog and package guidance; the goal extension remains compatible with current extension lifecycle and package install/update behavior.
41
+
42
+ ## 0.1.11 - 2026-05-21
43
+
44
+ - Cancels stale hidden goal continuations before they can reach the model after a goal is completed, cleared, or replaced.
45
+ - Keeps stale abort cleanup from charging tokens, pausing active replacement goals, persisting extra entries, or requeueing continuations during compaction and shutdown.
46
+ - Allows normal interactive and RPC prompts that paste continuation marker text to pass through instead of being treated as hidden extension follow-up work.
47
+ - Adds regression coverage for stale queued work across missing or delayed `agent_end`, late stale terminal events, compaction cleanup, and pasted marker input sources.
48
+
49
+ ## 0.1.10 - 2026-05-18
50
+
51
+ - Updated the local pi package baseline to `@earendil-works/*` `0.75.3` and refreshed the npm lockfile.
52
+ - Removed tracked CueLoop runtime state from the package and ignored local `.cueloop/` artifacts.
53
+
54
+
55
+ ## 0.1.9 - 2026-05-09
56
+
57
+ - Escapes goal objectives in hidden continuation and budget-limit prompts before embedding them in XML-style untrusted blocks.
58
+ - Keeps budget-limited goals from being paused or resumed back to active while they remain at or over budget.
59
+ - Sends a one-shot hidden budget-limit steering message when token accounting crosses the configured budget.
60
+ - Keeps ordinary user prompts from silently reactivating paused goals; session resume now prompts before restarting a paused goal.
61
+ - Returns Codex-shaped goal tool responses with `remainingTokens` and completion budget reports.
62
+ - Prevents tokens from an old in-flight turn from being charged to a replacement goal.
63
+ - Updates `/goal` summary and footer labels toward Codex-style status wording while retaining this package's 8000-character objective limit.
64
+
65
+ ## 0.1.8 - 2026-05-07
66
+
67
+ - Migrates the local pi development baseline and peer metadata from deprecated `@mariozechner/*` packages to maintained `@earendil-works/*` `0.74.0`.
68
+ - Regenerates the npm lockfile against the current stable dependency graph.
69
+
70
+ ## 0.1.7 - 2026-05-07
71
+
72
+ - Keeps active goals continuing after auto-compaction, including length-stop compactions.
73
+ - Prevents stale queued goal continuations from running after a goal is completed or changed.
74
+ - Strengthens completion-audit prompts and update-goal guidance so goals are marked complete only after verified completion.
75
+ - Avoids duplicate persisted completion entries from `update_goal`.
76
+
77
+ ## 0.1.6 - 2026-05-06
78
+
79
+ - Clarifies README install commands for npm, pinned npm, GitHub, and pinned GitHub package installs.
80
+
81
+ ## 0.1.5 - 2026-05-06
82
+
83
+ - Counts goal tokens from completed assistant input plus output usage instead of `usage.totalTokens`.
84
+ - Excludes cache read and cache write accounting channels from goal token budgets so cached provider tokens do not inflate sent and received totals.
85
+
86
+ ## 0.1.4 - 2026-05-06
87
+
88
+ - Pauses active goals when pi reports an aborted assistant turn, including user Esc aborts.
89
+ - Resumes paused goals automatically on the next user-driven agent start, while keeping `/goal resume` available.
90
+ - Prevents aborted turns from immediately queueing hidden continuation messages.
91
+
92
+ ## 0.1.3 - 2026-05-06
93
+
94
+ - Corrects the README behavior summary to describe completed assistant turn token accounting.
95
+
96
+ ## 0.1.2 - 2026-05-06
97
+
98
+ - Counts completed assistant turn usage via pi's `usage.totalTokens` instead of using context-window deltas, so goal token totals track tokens sent and received across compaction.
99
+ - Keeps elapsed-time accounting stable before and after compaction while continuing to persist active goal state.
100
+
101
+ ## 0.1.1 - 2026-05-06
102
+
103
+ - Marks pi runtime peer dependencies as optional so `pi install npm:pi-codex-goal` stays lightweight while still documenting the extension runtime contract.
104
+
105
+ ## 0.1.0 - 2026-05-06
106
+
107
+ - Initial public release.
108
+ - Adds Codex-style `/goal` tracking for pi.
109
+ - Adds model-callable `get_goal`, `create_goal`, and `update_goal` tools.
110
+ - Persists goal state in pi session custom entries for resume, reload, fork, tree navigation, and compaction.
111
+ - Starts and resumes goals with hidden follow-up messages so active objectives keep moving.
112
+ - Shows live elapsed active time and compact/exact token counts in the pi footer.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mitch Fultz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Pi-Goal
2
+
3
+ Pi-Goal brings Codex-style persistent goals to pi. It keeps a thread-scoped objective moving across turns, resumes, forks, compaction, provider errors, and queued user messages while preserving user control with `/goal pause`, `/goal resume`, and `/goal clear`.
4
+
5
+ ## Install and How to Use
6
+
7
+ ```sh
8
+ pi install npm:@kky42/pi-goal
9
+ ```
10
+
11
+ Use `/goal` from any pi session:
12
+
13
+ ```text
14
+ /goal
15
+ /goal Build the requested feature and verify it end to end
16
+ /goal pause
17
+ /goal resume
18
+ /goal clear
19
+ ```
20
+
21
+ `/goal <objective>` creates an active goal, displays the full goal summary, and asks the agent to continue immediately. Token budgets are set through the goal tool, matching Codex behavior, rather than parsed from `/goal --tokens`.
22
+
23
+ ## Pi-Goal vs. Codex Goal
24
+
25
+ | Area | Pi-Goal | Codex Goal |
26
+ | --- | --- | --- |
27
+ | Availability | Installable pi package: `pi install npm:@kky42/pi-goal` | Built into Codex |
28
+ | Goal start | `/goal <objective>` stores a thread-scoped goal and sends full goal context to the agent | Native goal initialization in the Codex thread |
29
+ | Continuation | Scheduler-owned hidden continuations after idle; queued user messages win | Native Codex continuation loop |
30
+ | State | Persisted as pi session custom entries, so it survives resume, fork, tree navigation, reload, and compaction | Stored in Codex's internal thread goal state |
31
+ | Completion | Agent marks `complete` only after a completion audit via `update_goal` | Same completion-audit contract |
32
+ | Blocked state | Agent can mark `blocked`; `/goal resume` reactivates paused or blocked goals | Same blocked/resume behavior |
33
+ | Token budget | Optional model-side budget; reaching it marks the goal `budgetLimited` and asks the agent to wrap up | Native Codex goal budget handling |
34
+ | Prompt caching | Historical continuation prompts stay byte-stable; stale continuations are dropped at runtime using metadata | Managed by Codex internals |
35
+ | Main difference | Codex-style goal behavior implemented as an inspectable pi extension | First-party Codex implementation |
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@kky42/pi-goal",
3
+ "version": "1.0.0",
4
+ "description": "Codex-style goal tracking and continuation for pi.",
5
+ "type": "module",
6
+ "author": "kky42",
7
+ "license": "MIT",
8
+ "main": "src/index.ts",
9
+ "files": [
10
+ "src",
11
+ "README.md",
12
+ "CHANGELOG.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "pi-package",
17
+ "pi",
18
+ "pi-extension",
19
+ "extension",
20
+ "codex",
21
+ "goal"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/kky42/pi-goal.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/kky42/pi-goal/issues"
29
+ },
30
+ "homepage": "https://github.com/kky42/pi-goal#readme",
31
+ "pi": {
32
+ "extensions": [
33
+ "./src/index.ts"
34
+ ]
35
+ },
36
+ "scripts": {
37
+ "test": "node --import tsx --test test/*.test.ts",
38
+ "test:e2e:real": "PI_GOAL_REAL_E2E=1 node --import tsx --test test/real-e2e.test.ts",
39
+ "typecheck": "tsc --noEmit",
40
+ "verify": "npm run typecheck && npm test"
41
+ },
42
+ "peerDependencies": {
43
+ "@earendil-works/pi-ai": "*",
44
+ "@earendil-works/pi-coding-agent": "*",
45
+ "typebox": "*"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "@earendil-works/pi-ai": {
49
+ "optional": true
50
+ },
51
+ "@earendil-works/pi-coding-agent": {
52
+ "optional": true
53
+ },
54
+ "typebox": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "devDependencies": {
59
+ "@earendil-works/pi-ai": "0.76.0",
60
+ "@earendil-works/pi-coding-agent": "0.76.0",
61
+ "@types/node": "^25.9.1",
62
+ "tsx": "^4.22.3",
63
+ "typebox": "1.1.38",
64
+ "typescript": "^6.0.3"
65
+ },
66
+ "engines": {
67
+ "node": ">=22.19.0 <27"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ },
72
+ "packageManager": "npm@11.0.0"
73
+ }
@@ -0,0 +1,107 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { formatGoalSummary } from "./format.js";
4
+ import { replaceGoal, updateGoalStatus } from "./state.js";
5
+ import type { GoalEntrySource, ThreadGoal } from "./types.js";
6
+
7
+ export interface CommandHost {
8
+ getGoal(): ThreadGoal | null;
9
+ setGoal(goal: ThreadGoal, source: GoalEntrySource, ctx: GoalCommandContext): void;
10
+ clearGoal(source: GoalEntrySource, ctx: GoalCommandContext): void;
11
+ requestContinuation(ctx: GoalCommandContext): void;
12
+ }
13
+
14
+ const COMMANDS = ["pause", "resume", "clear"] as const;
15
+ const GOLDEN_SET_BANNER = "\x1b[38;5;220mGoal set.\x1b[39m";
16
+
17
+ export type GoalCommandPi = Pick<ExtensionAPI, "registerCommand">;
18
+
19
+ export interface GoalCommandContext {
20
+ hasUI: boolean;
21
+ ui: Pick<ExtensionCommandContext["ui"], "confirm" | "notify" | "setStatus">;
22
+ }
23
+
24
+ function completions(prefix: string) {
25
+ return COMMANDS.filter((command) => command.startsWith(prefix)).map((command) => ({
26
+ value: command,
27
+ label: command,
28
+ description: `goal ${command}`,
29
+ }));
30
+ }
31
+
32
+ export async function handleGoalCommand(
33
+ _pi: GoalCommandPi,
34
+ host: CommandHost,
35
+ args: string,
36
+ ctx: GoalCommandContext,
37
+ ): Promise<void> {
38
+ const trimmed = args.trim();
39
+ if (trimmed.length === 0) {
40
+ ctx.ui.notify(formatGoalSummary(host.getGoal()));
41
+ return;
42
+ }
43
+
44
+ if (trimmed === "clear") {
45
+ const goal = host.getGoal();
46
+ if (!goal) {
47
+ ctx.ui.notify("No goal is set.", "warning");
48
+ return;
49
+ }
50
+ host.clearGoal("command", ctx);
51
+ ctx.ui.notify("Goal cleared.");
52
+ return;
53
+ }
54
+
55
+ if (trimmed === "pause" || trimmed === "resume") {
56
+ const current = host.getGoal();
57
+ const status = trimmed === "pause" ? "paused" : "active";
58
+ const result = updateGoalStatus(current, status);
59
+ if (!result.ok || !result.goal) {
60
+ ctx.ui.notify(result.message, "warning");
61
+ return;
62
+ }
63
+ host.setGoal(result.goal, "command", ctx);
64
+ ctx.ui.notify(result.message);
65
+ if (trimmed === "resume" && result.goal.status === "active") {
66
+ host.requestContinuation(ctx);
67
+ }
68
+ return;
69
+ }
70
+
71
+ const current = host.getGoal();
72
+ if (current && current.status !== "complete") {
73
+ if (!ctx.hasUI) {
74
+ ctx.ui.notify("Clear the existing goal before replacing it.", "error");
75
+ return;
76
+ }
77
+ const shouldReplace = await ctx.ui.confirm(
78
+ "Replace goal?",
79
+ `Current goal:\n${current.objective}\n\nNew goal:\n${trimmed}`,
80
+ );
81
+ if (!shouldReplace) {
82
+ ctx.ui.notify("Goal unchanged.");
83
+ return;
84
+ }
85
+ }
86
+
87
+ const result = replaceGoal(trimmed);
88
+ if (!result.ok || !result.goal) {
89
+ ctx.ui.notify(result.message, "error");
90
+ return;
91
+ }
92
+ host.setGoal(result.goal, "command", ctx);
93
+ ctx.ui.notify([GOLDEN_SET_BANNER, formatGoalSummary(result.goal)].join("\n"));
94
+ host.requestContinuation(ctx);
95
+ }
96
+
97
+ export function registerGoalCommand(pi: GoalCommandPi, host: CommandHost): void {
98
+ pi.registerCommand("goal", {
99
+ description: "Show or manage the current Codex-style goal.",
100
+ getArgumentCompletions(argumentPrefix) {
101
+ return completions(argumentPrefix.trim());
102
+ },
103
+ async handler(args: string, ctx: ExtensionCommandContext) {
104
+ await handleGoalCommand(pi, host, args, ctx);
105
+ },
106
+ });
107
+ }
@@ -0,0 +1,174 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { continuationGoalIdFromPrompt, continuationPrompt } from "./prompts.js";
4
+ import {
5
+ recoveryPhaseBlocksContinuation,
6
+ type GoalRecoveryMachineState,
7
+ } from "./recovery-machine.js";
8
+ import { isRecoveryPendingAttention } from "./recovery.js";
9
+ import { CONTINUATION_RETRY_MS } from "./runtime-config.js";
10
+ import type { StaleQueuedWorkGuard } from "./stale-queued-work-guard.js";
11
+ import { CUSTOM_ENTRY_TYPE, type ThreadGoal } from "./types.js";
12
+
13
+ interface ContinuationSchedulerDeps {
14
+ pi: Pick<ExtensionAPI, "sendMessage">;
15
+ getGoal: () => ThreadGoal | null;
16
+ getRecoveryState: () => GoalRecoveryMachineState;
17
+ staleQueuedWorkGuard: StaleQueuedWorkGuard;
18
+ getCurrentTurnIndex: () => number | null;
19
+ }
20
+
21
+ export function createContinuationScheduler(deps: ContinuationSchedulerDeps) {
22
+ let continuationQueuedFor: string | null = null;
23
+ let continuationScheduledFor: string | null = null;
24
+ let continuationTimer: ReturnType<typeof setTimeout> | null = null;
25
+ let passthroughContinuationInput: { text: string; turnIndex: number | null } | null = null;
26
+
27
+ const clearContinuationTimer = (): void => {
28
+ if (continuationTimer) {
29
+ clearTimeout(continuationTimer);
30
+ continuationTimer = null;
31
+ }
32
+ continuationScheduledFor = null;
33
+ };
34
+
35
+ const clearContinuationState = (): void => {
36
+ clearContinuationTimer();
37
+ continuationQueuedFor = null;
38
+ };
39
+
40
+ const clearContinuationStateFor = (goalId: string): void => {
41
+ if (continuationQueuedFor === goalId) {
42
+ continuationQueuedFor = null;
43
+ }
44
+ if (continuationScheduledFor === goalId) {
45
+ clearContinuationTimer();
46
+ }
47
+ };
48
+
49
+ const markContinuationQueued = (goalId: string): void => {
50
+ continuationQueuedFor = goalId;
51
+ };
52
+
53
+ const clearPassthroughContinuationInput = (): void => {
54
+ passthroughContinuationInput = null;
55
+ };
56
+
57
+ const bindPassthroughContinuationInputToTurn = (turnIndex: number): void => {
58
+ if (!passthroughContinuationInput) {
59
+ return;
60
+ }
61
+ if (passthroughContinuationInput.turnIndex === null) {
62
+ passthroughContinuationInput = { ...passthroughContinuationInput, turnIndex };
63
+ return;
64
+ }
65
+ if (passthroughContinuationInput.turnIndex !== turnIndex) {
66
+ clearPassthroughContinuationInput();
67
+ }
68
+ };
69
+
70
+ const isPassthroughContinuationInput = (text: string): boolean => {
71
+ if (!passthroughContinuationInput || passthroughContinuationInput.text !== text) {
72
+ return false;
73
+ }
74
+ const currentTurnIndex = deps.getCurrentTurnIndex();
75
+ return (
76
+ passthroughContinuationInput.turnIndex === null ||
77
+ passthroughContinuationInput.turnIndex === currentTurnIndex
78
+ );
79
+ };
80
+
81
+ const continuationGoalIdFromRuntimePrompt = (prompt: string): string | null => {
82
+ if (isPassthroughContinuationInput(prompt)) {
83
+ return null;
84
+ }
85
+ return continuationGoalIdFromPrompt(prompt);
86
+ };
87
+
88
+ const notePassthroughContinuationInput = (text: string): void => {
89
+ passthroughContinuationInput = { text, turnIndex: null };
90
+ };
91
+
92
+ const hasPendingRecoveryAttention = (): boolean => {
93
+ const goal = deps.getGoal();
94
+ return Boolean(goal?.status === "active" && isRecoveryPendingAttention(deps.getRecoveryState().attention));
95
+ };
96
+
97
+ const sendContinuation = (goalToContinue: ThreadGoal): void => {
98
+ continuationQueuedFor = goalToContinue.goalId;
99
+ deps.pi.sendMessage(
100
+ {
101
+ customType: CUSTOM_ENTRY_TYPE,
102
+ content: continuationPrompt(goalToContinue),
103
+ display: false,
104
+ details: { kind: "continuation", goalId: goalToContinue.goalId },
105
+ },
106
+ { triggerTurn: true, deliverAs: "followUp" },
107
+ );
108
+ };
109
+
110
+ const requestContinuation = (ctx: ExtensionContext): void => {
111
+ const goal = deps.getGoal();
112
+ if (
113
+ deps.staleQueuedWorkGuard.isBlockingContinuation() ||
114
+ !goal ||
115
+ goal.status !== "active" ||
116
+ continuationQueuedFor === goal.goalId ||
117
+ hasPendingRecoveryAttention() ||
118
+ recoveryPhaseBlocksContinuation(deps.getRecoveryState().phase)
119
+ ) {
120
+ return;
121
+ }
122
+
123
+ const goalId = goal.goalId;
124
+ if (ctx.hasPendingMessages()) {
125
+ if (continuationScheduledFor === goalId) {
126
+ clearContinuationTimer();
127
+ }
128
+ return;
129
+ }
130
+
131
+ if (!ctx.isIdle()) {
132
+ if (continuationScheduledFor === goalId) {
133
+ return;
134
+ }
135
+ continuationScheduledFor = goalId;
136
+ continuationTimer = setTimeout(() => {
137
+ continuationTimer = null;
138
+ continuationScheduledFor = null;
139
+ requestContinuation(ctx);
140
+ }, CONTINUATION_RETRY_MS);
141
+ continuationTimer.unref?.();
142
+ return;
143
+ }
144
+
145
+ clearContinuationTimer();
146
+ const currentGoal = deps.getGoal();
147
+ if (
148
+ deps.staleQueuedWorkGuard.isBlockingContinuation() ||
149
+ !ctx.isIdle() ||
150
+ ctx.hasPendingMessages() ||
151
+ !currentGoal ||
152
+ currentGoal.status !== "active" ||
153
+ currentGoal.goalId !== goalId ||
154
+ continuationQueuedFor === goalId ||
155
+ hasPendingRecoveryAttention() ||
156
+ recoveryPhaseBlocksContinuation(deps.getRecoveryState().phase)
157
+ ) {
158
+ return;
159
+ }
160
+ sendContinuation(currentGoal);
161
+ };
162
+
163
+ return {
164
+ bindPassthroughContinuationInputToTurn,
165
+ clearContinuationState,
166
+ clearContinuationStateFor,
167
+ clearContinuationTimer,
168
+ clearPassthroughContinuationInput,
169
+ continuationGoalIdFromRuntimePrompt,
170
+ markContinuationQueued,
171
+ notePassthroughContinuationInput,
172
+ requestContinuation,
173
+ };
174
+ }