@jameslovespancakes/pi-plus 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,7 +129,9 @@ the one line to run, and verifies.
129
129
  `remote_test` snapshots your working tree, admission-checks CPU/GPU/disk,
130
130
  reserves a slot, and **deletes the uploaded source the moment the run ends**,
131
131
  keeping only `test.log` and `result.json`. Cleanup runs on the worker, so it
132
- still happens if your laptop sleeps.
132
+ still happens if your laptop sleeps. Worker capacity is queried only by
133
+ `remote_status` or `remote_test`; Pi Plus never injects SSH status into ordinary
134
+ chat turns.
133
135
 
134
136
  ### Coordinate multiple agents
135
137
 
@@ -152,26 +154,9 @@ across every running pi agent. `/board` opens the messaging view:
152
154
 
153
155
  `/board setup` installs the server locally (pi starts it each session) or onto
154
156
  any Mac or Linux host over SSH, where launchd or systemd brings it back after a
155
- reboot.
156
-
157
- ### Compact without deleting the source
158
-
159
- Better Compact replaces Pi's normal compaction with a reversible local archive,
160
- deterministic protection and extractive compression. Jev mode adds six-signal
161
- routing through OpenRouter's decisions API; Jev ranks compression but cannot
162
- override protected facts or authorize source deletion.
163
-
164
- ```
165
- /compact better on # switch to local deterministic routing and compact now
166
- /compact better jev # switch to Jev routing and compact now
167
- /compact better off # restore Pi compaction and compact now
168
- ```
169
-
170
- The selected mode persists for later manual and automatic compactions. Jev mode
171
- requires `OPENROUTER_API_KEY` (or OpenRouter auth configured in Pi). Original
172
- chunks stay under `~/.pi/agent/super-context/archives/`; the
173
- `super_context_recall` tool performs bounded retrieval from the current branch's
174
- checkpoint.
157
+ reboot. Board state is returned only when `agent_board` is called or a real
158
+ board message is delivered; background presence snapshots are not added to
159
+ model context.
175
160
 
176
161
  ### Orchestrate repeatable workflows
177
162
 
@@ -213,12 +198,11 @@ with workflow options when a task needs them.
213
198
  | `/remote add \| rename \| remove` | jump to one step |
214
199
  | `/board` | live agent board UI |
215
200
  | `/board setup \| restart \| clear \| status` | manage the board server |
216
- | `/compact better on \| off \| jev` | select reversible compaction and compact now |
217
201
  | `/workflow` | open the running workflow agent board |
218
202
  | `/workflow <name> [args]` | run a bundled workflow |
219
203
 
220
204
  **Tools available to the agent:** `workflow`, `list_models`, `agent_board`,
221
- `remote_status`, `remote_test`, `super_context_recall`.
205
+ `remote_status`, `remote_test`.
222
206
 
223
207
  ---
224
208
 
@@ -230,8 +214,7 @@ Everything lives in one file, `~/.pi/agent/pi-plus.json`, created on first use:
230
214
  {
231
215
  "env": { "ARTIFICIAL_ANALYSIS_API_KEY": "aa_…", "AGENT_BOARD_URL": "ws://…" },
232
216
  "policy": { "autoApprove": [], "requireApproval": [], "deny": [] },
233
- "remote": { "workers": [] },
234
- "compact": { "better": "off" }
217
+ "remote": { "workers": [] }
235
218
  }
236
219
  ```
237
220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jameslovespancakes/pi-plus",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "type": "module",
5
5
  "description": "pi and more",
6
6
  "license": "MIT",
@@ -37,7 +37,6 @@
37
37
  "./src/domains/setup/index.ts",
38
38
  "./src/domains/subscriptions/index.ts",
39
39
  "./src/domains/models/index.ts",
40
- "./src/domains/compact/index.ts",
41
40
  "./src/domains/workflows/index.ts",
42
41
  "./src/domains/agents/index.ts",
43
42
  "./src/domains/remote/index.ts"
@@ -50,10 +49,10 @@
50
49
  "ws": "^8.18.3"
51
50
  },
52
51
  "peerDependencies": {
53
- "@earendil-works/pi-agent-core": "*",
54
- "@earendil-works/pi-ai": "*",
55
- "@earendil-works/pi-coding-agent": "*",
56
- "@earendil-works/pi-tui": "*",
52
+ "@earendil-works/pi-agent-core": "^0.86.1",
53
+ "@earendil-works/pi-ai": "^0.86.1",
54
+ "@earendil-works/pi-coding-agent": "^0.86.1",
55
+ "@earendil-works/pi-tui": "^0.86.1",
57
56
  "typebox": "*"
58
57
  },
59
58
  "scripts": {
@@ -15,22 +15,14 @@ export interface PolicySection {
15
15
  }
16
16
 
17
17
  export interface RemoteSection {
18
- injectStatus?: boolean;
19
18
  defaults?: Record<string, unknown>;
20
19
  workers: Record<string, unknown>[];
21
20
  }
22
21
 
23
- export type BetterCompactMode = "off" | "on" | "jev";
24
-
25
- export interface CompactSection {
26
- better: BetterCompactMode;
27
- }
28
-
29
22
  export interface PiPlusConfig {
30
23
  env: Record<string, string>;
31
24
  policy: PolicySection;
32
25
  remote: RemoteSection;
33
- compact: CompactSection;
34
26
  }
35
27
 
36
28
  const DEFAULTS: PiPlusConfig = {
@@ -40,8 +32,7 @@ const DEFAULTS: PiPlusConfig = {
40
32
  requireApproval: ["openrouter/*", "google/*", "openai/*"],
41
33
  deny: [],
42
34
  },
43
- remote: { injectStatus: true, workers: [] },
44
- compact: { better: "off" },
35
+ remote: { workers: [] },
45
36
  };
46
37
 
47
38
  /** Legacy file -> section, applied only when that section is still absent. */
@@ -90,7 +81,7 @@ const MIGRATIONS: { file: string; apply: (raw: any, into: PiPlusConfig) => boole
90
81
  file: "remote.json",
91
82
  apply: (raw, into) => {
92
83
  if (into.remote.workers.length > 0 || !Array.isArray(raw?.workers)) return false;
93
- into.remote = { injectStatus: raw.injectStatus !== false, defaults: raw.defaults, workers: raw.workers };
84
+ into.remote = { defaults: raw.defaults, workers: raw.workers };
94
85
  return true;
95
86
  },
96
87
  },
@@ -98,7 +89,7 @@ const MIGRATIONS: { file: string; apply: (raw: any, into: PiPlusConfig) => boole
98
89
  file: "remote-workers.json",
99
90
  apply: (raw, into) => {
100
91
  if (into.remote.workers.length > 0 || !Array.isArray(raw?.workers)) return false;
101
- into.remote = { injectStatus: raw.injectStatus !== false, defaults: raw.defaults, workers: raw.workers };
92
+ into.remote = { defaults: raw.defaults, workers: raw.workers };
102
93
  return true;
103
94
  },
104
95
  },
@@ -111,7 +102,6 @@ export function configPath(): string {
111
102
  }
112
103
 
113
104
  function normalize(raw: Partial<PiPlusConfig> | undefined): PiPlusConfig {
114
- const better = raw?.compact?.better;
115
105
  return {
116
106
  env: raw?.env && typeof raw.env === "object" ? { ...raw.env } : {},
117
107
  policy: {
@@ -122,13 +112,9 @@ function normalize(raw: Partial<PiPlusConfig> | undefined): PiPlusConfig {
122
112
  deny: Array.isArray(raw?.policy?.deny) ? raw.policy.deny : [],
123
113
  },
124
114
  remote: {
125
- injectStatus: raw?.remote?.injectStatus !== false,
126
115
  defaults: raw?.remote?.defaults,
127
116
  workers: Array.isArray(raw?.remote?.workers) ? raw.remote.workers : [],
128
117
  },
129
- compact: {
130
- better: better === "on" || better === "jev" ? better : "off",
131
- },
132
118
  };
133
119
  }
134
120
 
@@ -137,16 +123,16 @@ export function readConfig(): PiPlusConfig {
137
123
 
138
124
  const path = configPath();
139
125
  const exists = existsSync(path);
140
- const config = normalize(exists ? readJson<Partial<PiPlusConfig>>(path, {}) : undefined);
126
+ const raw = exists ? readJson<Partial<PiPlusConfig>>(path, {}) : undefined;
127
+ const config = normalize(raw);
141
128
 
142
- // Only consider legacy sources when the unified file is absent or partial.
143
129
  let migrated = false;
144
130
  for (const migration of MIGRATIONS) {
145
131
  const legacyPath = agentPath(migration.file);
146
132
  if (!existsSync(legacyPath)) continue;
147
- const raw = readJson<any>(legacyPath, undefined);
148
- if (raw === undefined) continue;
149
- if (migration.apply(raw, config)) migrated = true;
133
+ const legacy = readJson<any>(legacyPath, undefined);
134
+ if (legacy === undefined) continue;
135
+ if (migration.apply(legacy, config)) migrated = true;
150
136
  }
151
137
 
152
138
  if (!exists || migrated) writeJson(path, config, true);
@@ -1,30 +1,5 @@
1
1
  const DELIVERY_TEXT_MAX = 2_000;
2
2
 
3
- export interface SnapshotAgent {
4
- sessionId: string;
5
- alias?: string;
6
- host: string;
7
- branch?: string;
8
- repo?: string;
9
- commit?: string;
10
- state: "idle" | "thinking" | "tool";
11
- lastTool?: string;
12
- coordinator?: string | null;
13
- reports?: string[];
14
- }
15
-
16
- export interface SnapshotSelf {
17
- branch?: string;
18
- repo?: string;
19
- commit?: string;
20
- }
21
-
22
- export interface SnapshotCoordination {
23
- coordinator?: string | null;
24
- reports?: string[];
25
- repoThread?: string;
26
- }
27
-
28
3
  export interface DeliveryMessage {
29
4
  senderId: string;
30
5
  senderAlias?: string;
@@ -47,39 +22,6 @@ function cleanLine(value: unknown, max: number): string {
47
22
  return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
48
23
  }
49
24
 
50
- function compactTool(lastTool?: string): string | undefined {
51
- if (!lastTool) return undefined;
52
- const separator = lastTool.indexOf(":");
53
- return cleanLine(separator < 0 ? lastTool : lastTool.slice(0, separator), 32);
54
- }
55
-
56
- export function formatBoardSnapshot(
57
- agents: readonly SnapshotAgent[],
58
- self: SnapshotSelf,
59
- coordination: SnapshotCoordination,
60
- ): string {
61
- const commit = self.commit?.slice(0, 12) || "no-commit";
62
- const meta = [
63
- `${self.branch || "-"}@${commit}`,
64
- `${agents.length} peer${agents.length === 1 ? "" : "s"}`,
65
- coordination.repoThread ? `room=${coordination.repoThread}` : undefined,
66
- coordination.coordinator ? `reports-to=${coordination.coordinator}` : undefined,
67
- coordination.reports?.length ? `reports=${coordination.reports.join(",")}` : undefined,
68
- ].filter(Boolean).join(" ");
69
- const lines = [`[board ${meta}]`];
70
- for (const agent of agents.slice(0, 12)) {
71
- const agentCommit = agent.commit?.slice(0, 12) || "no-commit";
72
- const sameLine = agent.repo && self.repo === agent.repo && agent.branch === self.branch;
73
- const drift = sameLine && agent.commit && self.commit && agent.commit !== self.commit ? " !commit" : "";
74
- const tool = agent.state === "tool" ? compactTool(agent.lastTool) : undefined;
75
- const activity = tool ? `tool:${tool}` : agent.state;
76
- const role = agent.reports?.length ? " coord" : agent.coordinator ? ` ->${agent.coordinator}` : "";
77
- lines.push(`- ${agent.alias || agent.sessionId.slice(0, 8)}@${agent.host} ${activity} ${agent.branch || "-"}@${agentCommit}${drift}${role}`);
78
- }
79
- if (agents.length > 12) lines.push(`- +${agents.length - 12} more; use agent_board agents`);
80
- return lines.join("\n");
81
- }
82
-
83
25
  function compactDeliveryText(text: string): string {
84
26
  const clean = text.replace(/\r\n?/g, "\n").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
85
27
  if (clean.length <= DELIVERY_TEXT_MAX) return clean;
@@ -11,11 +11,10 @@ import { Type } from "typebox";
11
11
  import WebSocket from "ws";
12
12
  import { env } from "../../core/env.ts";
13
13
  import { handleBoardAdmin, registerBoardLifecycle } from "./board-setup.ts";
14
- import { formatBoardDeliveries, formatBoardSnapshot, type BoardDelivery } from "./format.ts";
14
+ import { formatBoardDeliveries, type BoardDelivery } from "./format.ts";
15
15
 
16
16
  const HEARTBEAT_MS = 8_000;
17
17
  const REQUEST_TIMEOUT_MS = 2_000;
18
- const SNAPSHOT_CACHE_MS = 1_500;
19
18
  const MESSAGE_MAX = 8_000;
20
19
  const DELIVERY_DEBOUNCE_MS = 250;
21
20
  const DELIVERY_BATCH_MAX = 8;
@@ -304,8 +303,6 @@ export default function (pi: ExtensionAPI) {
304
303
  let ctx: ExtensionContext | undefined;
305
304
  let self: AgentInfo | undefined;
306
305
  let gitInfo: GitInfo = {};
307
- let snapshot: { at: number; agents: AgentInfo[] } = { at: 0, agents: [] };
308
- let lastInjectedSnapshot: string | undefined;
309
306
  let pendingDeliveries: BoardDelivery[] = [];
310
307
  let deliveryTimer: ReturnType<typeof setTimeout> | undefined;
311
308
 
@@ -321,12 +318,6 @@ export default function (pi: ExtensionAPI) {
321
318
  self = { ...self, ...gitInfo };
322
319
  client.presence(gitInfo);
323
320
  };
324
- const snapshotAgents = async (): Promise<AgentInfo[]> => {
325
- if (Date.now() - snapshot.at > SNAPSHOT_CACHE_MS) {
326
- snapshot = { at: Date.now(), agents: await client.request("agents", {}, 500) };
327
- }
328
- return snapshot.agents;
329
- };
330
321
  const flushDeliveries = () => {
331
322
  if (deliveryTimer) clearTimeout(deliveryTimer);
332
323
  deliveryTimer = undefined;
@@ -354,7 +345,6 @@ export default function (pi: ExtensionAPI) {
354
345
 
355
346
  client.on((event) => {
356
347
  if (event.t === "connection") {
357
- if (event.connected) { snapshot.at = 0; lastInjectedSnapshot = undefined; }
358
348
  updateStatus();
359
349
  return;
360
350
  }
@@ -374,19 +364,9 @@ export default function (pi: ExtensionAPI) {
374
364
  });
375
365
  pi.on("session_info_changed", async (event) => { if (self) { self.alias = event.name; client.presence({ alias: event.name }); } });
376
366
  pi.on("model_select", async (event) => client.presence({ model: `${event.model.provider}/${event.model.id}` }));
377
- pi.on("before_agent_start", async (event) => {
367
+ pi.on("before_agent_start", (event) => {
378
368
  client.presence({ state: "thinking", lastPrompt: cleanLine(event.prompt) });
379
369
  client.activity("prompt", event.prompt);
380
- if (!client.connected) return;
381
- try {
382
- const content = formatBoardSnapshot(await snapshotAgents(), gitInfo, client.coordination);
383
- if (content === lastInjectedSnapshot) return;
384
- lastInjectedSnapshot = content;
385
- // Persist one compact snapshot at a user-turn boundary. A transient message
386
- // appended in `context` becomes Anthropic's final cache breakpoint but is
387
- // absent from the next transcript, forcing a conversation-cache miss.
388
- return { message: { customType: "agent-board-snapshot", content, display: false, details: {} } };
389
- } catch { return; }
390
370
  });
391
371
  pi.on("tool_execution_start", async (event) => {
392
372
  const lastTool = cleanLine(`${event.toolName}: ${JSON.stringify(event.args)}`, 100);
@@ -394,8 +374,6 @@ export default function (pi: ExtensionAPI) {
394
374
  });
395
375
  pi.on("tool_execution_end", async () => client.presence({ state: "thinking" }));
396
376
  pi.on("agent_settled", async () => { client.presence({ state: "idle", lastTool: undefined }); refreshGit(); });
397
- pi.on("session_compact", async () => { lastInjectedSnapshot = undefined; });
398
- pi.on("session_tree", async () => { lastInjectedSnapshot = undefined; });
399
377
  pi.on("session_shutdown", async (_event, eventCtx) => {
400
378
  if (deliveryTimer) clearTimeout(deliveryTimer);
401
379
  deliveryTimer = undefined; pendingDeliveries = [];
@@ -20,7 +20,6 @@ export interface RemoteWorkerRecord {
20
20
  }
21
21
 
22
22
  export interface RemoteSettings {
23
- injectStatus: boolean;
24
23
  defaults: Record<string, unknown>;
25
24
  workers: RemoteWorkerRecord[];
26
25
  }
@@ -28,7 +27,6 @@ export interface RemoteSettings {
28
27
  export function readRemote(): RemoteSettings {
29
28
  const section = readConfig().remote;
30
29
  return {
31
- injectStatus: section.injectStatus !== false,
32
30
  defaults: (section.defaults as Record<string, unknown>) ?? {},
33
31
  workers: (section.workers as RemoteWorkerRecord[]) ?? [],
34
32
  };
@@ -31,7 +31,6 @@ interface Limits {
31
31
  gpuMemoryBlockPercent: number;
32
32
  memoryBlockPercent: number;
33
33
  minimumFreeDiskGB: number;
34
- statusCacheSeconds: number;
35
34
  retentionHours: number;
36
35
  }
37
36
 
@@ -58,7 +57,6 @@ interface Worker extends Omit<WorkerInput, keyof Limits | "enabled">, Limits {
58
57
  }
59
58
 
60
59
  interface Config {
61
- injectStatus: boolean;
62
60
  workers: Worker[];
63
61
  }
64
62
 
@@ -100,7 +98,6 @@ const DEFAULT_LIMITS: Limits = {
100
98
  gpuMemoryBlockPercent: 90,
101
99
  memoryBlockPercent: 90,
102
100
  minimumFreeDiskGB: 10,
103
- statusCacheSeconds: 30,
104
101
  retentionHours: 24,
105
102
  };
106
103
 
@@ -117,8 +114,6 @@ const HARD_WALK_EXCLUDES = new Set([
117
114
  "remote_tests",
118
115
  ]);
119
116
 
120
- let statusCache: { expiresAt: number; key: string; statuses: WorkerStatus[] } | undefined;
121
-
122
117
  function finiteNumber(value: unknown, fallback: number, min: number, max: number): number {
123
118
  return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
124
119
  }
@@ -139,7 +134,6 @@ async function loadConfig(): Promise<Config> {
139
134
  ),
140
135
  memoryBlockPercent: finiteNumber(defaults.memoryBlockPercent, DEFAULT_LIMITS.memoryBlockPercent, 1, 100),
141
136
  minimumFreeDiskGB: finiteNumber(defaults.minimumFreeDiskGB, DEFAULT_LIMITS.minimumFreeDiskGB, 0, 100000),
142
- statusCacheSeconds: finiteNumber(defaults.statusCacheSeconds, DEFAULT_LIMITS.statusCacheSeconds, 1, 600),
143
137
  retentionHours: finiteNumber(defaults.retentionHours, DEFAULT_LIMITS.retentionHours, 1, 24 * 365),
144
138
  };
145
139
  const names = new Set<string>();
@@ -172,11 +166,10 @@ async function loadConfig(): Promise<Config> {
172
166
  ),
173
167
  memoryBlockPercent: finiteNumber(raw.memoryBlockPercent, defaultLimits.memoryBlockPercent, 1, 100),
174
168
  minimumFreeDiskGB: finiteNumber(raw.minimumFreeDiskGB, defaultLimits.minimumFreeDiskGB, 0, 100000),
175
- statusCacheSeconds: finiteNumber(raw.statusCacheSeconds, defaultLimits.statusCacheSeconds, 1, 600),
176
169
  retentionHours: finiteNumber(raw.retentionHours, defaultLimits.retentionHours, 1, 24 * 365),
177
170
  };
178
171
  });
179
- return { injectStatus: parsed.injectStatus !== false, workers };
172
+ return { workers };
180
173
  }
181
174
 
182
175
  /** `-i`/`-p` only when the worker was added without an ~/.ssh/config entry. */
@@ -331,27 +324,15 @@ async function probeWorker(worker: Worker): Promise<WorkerStatus> {
331
324
  }
332
325
  }
333
326
 
334
- async function probeWorkers(config: Config, force = false): Promise<WorkerStatus[]> {
327
+ async function probeWorkers(config: Config): Promise<WorkerStatus[]> {
335
328
  const active = config.workers.filter((worker) => worker.enabled);
336
- if (active.length === 0) return [];
337
- const key = JSON.stringify(active.map((worker) => [worker.name, worker.ssh, worker.root]));
338
- const ttlSeconds = Math.min(...active.map((worker) => worker.statusCacheSeconds));
339
- if (!force && statusCache && statusCache.key === key && statusCache.expiresAt > Date.now()) return statusCache.statuses;
340
- const statuses = await Promise.all(active.map(probeWorker));
341
- statusCache = { key, statuses, expiresAt: Date.now() + ttlSeconds * 1000 };
342
- return statuses;
329
+ return Promise.all(active.map(probeWorker));
343
330
  }
344
331
 
345
332
  function metric(value: number | undefined): string {
346
333
  return value === undefined ? "?" : `${Math.round(value)}%`;
347
334
  }
348
335
 
349
- function compactStatus(status: WorkerStatus): string {
350
- const gpu = status.gpuPercent === undefined ? "GPU ?" : `GPU ${metric(status.gpuPercent)}`;
351
- const reason = status.reasons.length ? ` (${status.reasons.join(", ")})` : "";
352
- return `${status.name} ${status.state.toUpperCase()} CPU ${metric(status.cpuPercent)} MEM ${metric(status.memoryPercent)} ${gpu} active-jobs ${status.jobs ?? "?"}${reason}`;
353
- }
354
-
355
336
  function detailedStatuses(statuses: WorkerStatus[]): string {
356
337
  return statuses
357
338
  .map((status) => {
@@ -696,7 +677,6 @@ export default function remoteJobsExtension(pi: ExtensionAPI) {
696
677
  const selected = params.host ? enabled.filter((worker) => worker.name === params.host) : enabled;
697
678
  if (params.host && selected.length === 0) throw new Error(`Unknown remote worker: ${params.host}`);
698
679
  const statuses = await Promise.all(selected.map(probeWorker));
699
- statusCache = undefined;
700
680
  return {
701
681
  content: [{ type: "text", text: detailedStatuses(statuses) }],
702
682
  details: { statuses },
@@ -745,7 +725,7 @@ export default function remoteJobsExtension(pi: ExtensionAPI) {
745
725
  if (requestedHost !== "auto" && !enabledWorkers.some((worker) => worker.name === requestedHost)) {
746
726
  throw new Error(`Unknown remote worker: ${requestedHost}. Available: ${enabledWorkers.map((w) => w.name).join(", ")}`);
747
727
  }
748
- let statuses = await probeWorkers(config, true);
728
+ const statuses = await probeWorkers(config);
749
729
  let worker = pickWorker(config, statuses, requestedHost, params.requiresGpu ?? false);
750
730
  if (!worker) return blockedResult(undefined, undefined, detailedStatuses(statuses));
751
731
  let workerStatus = statuses.find((status) => status.name === worker!.name);
@@ -818,7 +798,6 @@ export default function remoteJobsExtension(pi: ExtensionAPI) {
818
798
  text += `\n\n[Output truncated; full log: ${remoteJob}/test.log]`;
819
799
  }
820
800
  void runSsh(worker, "bash -s", { input: cleanupScript(worker, snapshot.repoName), timeoutSeconds: 15 }).catch(() => {});
821
- statusCache = undefined;
822
801
  return {
823
802
  content: [{ type: "text", text }],
824
803
  details: {
@@ -847,20 +826,4 @@ export default function remoteJobsExtension(pi: ExtensionAPI) {
847
826
  },
848
827
  });
849
828
 
850
- pi.on("before_agent_start", async (event) => {
851
- try {
852
- const config = await loadConfig();
853
- if (!config.injectStatus) return;
854
- const statuses = await probeWorkers(config);
855
- if (statuses.length === 0) return;
856
- const line = statuses.map(compactStatus).join("; ");
857
- return {
858
- systemPrompt:
859
- event.systemPrompt +
860
- `\n\nRemote worker capacity (recent sample): ${line}. remote_test always performs a fresh hard admission check. Never retry a BLOCKED worker immediately; choose another READY worker or run locally.`,
861
- };
862
- } catch {
863
- return;
864
- }
865
- });
866
829
  }
@@ -8,6 +8,7 @@ import {
8
8
  export const AGENT_RETRY_BASE_DELAY_MS = 1_000;
9
9
  export const AGENT_RETRY_MAX_DELAY_MS = 30_000;
10
10
  export const WORKFLOW_PROVIDER_ERROR_CODE = "WORKFLOW_PROVIDER_ERROR";
11
+ const CODEX_ACCESS_VERIFICATION_ERROR = /Unable to verify\s+[^.\r\n]{1,120}\s+access\.\s*Please try again\.?/i;
11
12
 
12
13
  export interface AgentRetryScheduler {
13
14
  sleep(delayMs: number, signal: AbortSignal | undefined): Promise<void>;
@@ -25,12 +26,11 @@ export interface ProviderErrorDetails {
25
26
  export class WorkflowProviderError extends Error {
26
27
  override readonly name = "WorkflowProviderError";
27
28
  readonly code = WORKFLOW_PROVIDER_ERROR_CODE;
29
+ readonly details: ProviderErrorDetails;
28
30
 
29
- constructor(
30
- message: string,
31
- readonly details: ProviderErrorDetails,
32
- ) {
31
+ constructor(message: string, details: ProviderErrorDetails) {
33
32
  super(message);
33
+ this.details = details;
34
34
  }
35
35
 
36
36
  get retryable(): boolean {
@@ -79,13 +79,18 @@ export function providerErrorFromMessages(
79
79
  if (usageLimit && options.pauseOnUsageLimit) return usageLimit;
80
80
  const message = messages.findLast(isAssistantMessage);
81
81
  if (!message || message.stopReason !== "error") return undefined;
82
- const errorMessage = typeof message.errorMessage === "string" && message.errorMessage.length > 0
82
+ const providerMessage = typeof message.errorMessage === "string" && message.errorMessage.length > 0
83
83
  ? message.errorMessage
84
84
  : "Provider session ended with an unspecified error.";
85
- const retryable = isRetryableAssistantError(message as AssistantMessage);
86
85
  const provider = stringDetail(message.provider);
87
86
  const model = stringDetail(message.model);
88
87
  const api = stringDetail(message.api);
88
+ const codexAccessVerification = provider === "openai-codex"
89
+ && CODEX_ACCESS_VERIFICATION_ERROR.test(providerMessage);
90
+ const retryable = isRetryableAssistantError(message as AssistantMessage) || codexAccessVerification;
91
+ const errorMessage = codexAccessVerification && model
92
+ ? `Codex temporarily could not verify access for the selected model ${provider}/${model}. No alternate model was requested. Provider response: ${providerMessage}`
93
+ : providerMessage;
89
94
  return new WorkflowProviderError(errorMessage, {
90
95
  stopReason: "error",
91
96
  retryable,
@@ -1,140 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { basename } from "node:path";
4
- import { agentPath, writeJson } from "../../core/store.ts";
5
- import type { ArchiveRecord, SemanticChunk, SuperContextArchive } from "./types.ts";
6
-
7
- function safeSessionId(sessionId: string): string {
8
- const readable = sessionId.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 80) || "session";
9
- const suffix = createHash("sha256").update(sessionId).digest("hex").slice(0, 10);
10
- return `${readable}-${suffix}`;
11
- }
12
-
13
- export function archivePath(sessionId: string): string {
14
- return agentPath("super-context", "archives", `${safeSessionId(sessionId)}.json`);
15
- }
16
-
17
- function emptyArchive(sessionId: string): SuperContextArchive {
18
- return { version: 1, sessionId, records: [], checkpoints: {} };
19
- }
20
-
21
- function isArchiveRecord(value: unknown): value is ArchiveRecord {
22
- if (!value || typeof value !== "object") return false;
23
- const record = value as Partial<ArchiveRecord>;
24
- return typeof record.id === "string"
25
- && typeof record.hash === "string"
26
- && typeof record.text === "string"
27
- && typeof record.role === "string"
28
- && typeof record.ordinal === "number"
29
- && typeof record.archivedAt === "string";
30
- }
31
-
32
- /** Refuse to overwrite a corrupt archive: source recoverability wins over convenience. */
33
- export function loadArchive(sessionId: string): SuperContextArchive {
34
- const path = archivePath(sessionId);
35
- if (!existsSync(path)) return emptyArchive(sessionId);
36
-
37
- let parsed: unknown;
38
- try {
39
- parsed = JSON.parse(readFileSync(path, "utf8"));
40
- } catch (error) {
41
- throw new Error(`Super Context archive is unreadable; refusing to overwrite ${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
42
- }
43
- if (!parsed || typeof parsed !== "object") throw new Error(`Invalid Super Context archive: ${path}`);
44
- const candidate = parsed as Partial<SuperContextArchive>;
45
- if (candidate.version !== 1 || candidate.sessionId !== sessionId || !Array.isArray(candidate.records)) {
46
- throw new Error(`Unsupported Super Context archive format: ${path}`);
47
- }
48
- if (!candidate.records.every(isArchiveRecord)) throw new Error(`Invalid Super Context archive records: ${path}`);
49
- const checkpoints = candidate.checkpoints;
50
- if (!checkpoints || typeof checkpoints !== "object" || Array.isArray(checkpoints)) {
51
- throw new Error(`Invalid Super Context archive checkpoints: ${path}`);
52
- }
53
- for (const ids of Object.values(checkpoints)) {
54
- if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string")) {
55
- throw new Error(`Invalid Super Context archive checkpoint: ${path}`);
56
- }
57
- }
58
- return candidate as SuperContextArchive;
59
- }
60
-
61
- function addChunks(archive: SuperContextArchive, chunks: readonly SemanticChunk[]): void {
62
- const byId = new Map(archive.records.map((record) => [record.id, record]));
63
- let ordinal = archive.records.reduce((maximum, record) => Math.max(maximum, record.ordinal), -1) + 1;
64
- const archivedAt = new Date().toISOString();
65
- for (const chunk of chunks) {
66
- const existing = byId.get(chunk.id);
67
- if (existing) {
68
- if (existing.hash !== chunk.hash || existing.text !== chunk.text) {
69
- throw new Error(`Super Context archive ID collision for ${chunk.id}`);
70
- }
71
- continue;
72
- }
73
- const record: ArchiveRecord = { ...chunk, ordinal, archivedAt };
74
- ordinal += 1;
75
- archive.records.push(record);
76
- byId.set(record.id, record);
77
- }
78
- }
79
-
80
- function checkpointHash(recordIds: readonly string[]): string {
81
- const digest = createHash("sha256").update(recordIds.join("\0")).digest("hex");
82
- return `SCC-${digest.slice(0, 20)}`;
83
- }
84
-
85
- export interface PersistedCheckpoint {
86
- archive: SuperContextArchive;
87
- checkpointId: string;
88
- records: ArchiveRecord[];
89
- duplicateChunks: number;
90
- file: string;
91
- }
92
-
93
- /** Add-only persistence for immutable records and content-addressed checkpoints. */
94
- export function persistCheckpoint(
95
- sessionId: string,
96
- chunks: readonly SemanticChunk[],
97
- priorRecordIds: readonly string[],
98
- ): PersistedCheckpoint {
99
- const archive = loadArchive(sessionId);
100
- const currentIds = chunks.map((chunk) => chunk.id);
101
- const allRecordIds = [...priorRecordIds, ...currentIds];
102
- const duplicateChunks = allRecordIds.length - new Set(allRecordIds).size;
103
- addChunks(archive, chunks);
104
-
105
- const knownIds = new Set(archive.records.map((record) => record.id));
106
- const recordIds = [...new Set(allRecordIds)];
107
- const missing = recordIds.filter((id) => !knownIds.has(id));
108
- if (missing.length > 0) throw new Error(`Super Context archive is missing ${missing.length} source record(s)`);
109
-
110
- const checkpointId = checkpointHash(recordIds);
111
- const existing = archive.checkpoints[checkpointId];
112
- if (existing && JSON.stringify(existing) !== JSON.stringify(recordIds)) {
113
- throw new Error(`Super Context checkpoint collision for ${checkpointId}`);
114
- }
115
- archive.checkpoints[checkpointId] ??= recordIds;
116
-
117
- const file = archivePath(sessionId);
118
- if (!writeJson(file, archive, false, 0o600)) throw new Error(`Failed to persist Super Context archive: ${file}`);
119
- const byId = new Map(archive.records.map((record) => [record.id, record]));
120
- return {
121
- archive,
122
- checkpointId,
123
- records: recordIds.map((id) => byId.get(id)!),
124
- duplicateChunks,
125
- file,
126
- };
127
- }
128
-
129
- export function checkpointRecords(archive: SuperContextArchive, checkpointId: string | undefined): ArchiveRecord[] {
130
- if (!checkpointId) return [];
131
- const ids = archive.checkpoints[checkpointId];
132
- if (!ids) return [];
133
- const byId = new Map(archive.records.map((record) => [record.id, record]));
134
- const records = ids.map((id) => byId.get(id)).filter((record): record is ArchiveRecord => record !== undefined);
135
- return records.length === ids.length ? records : [];
136
- }
137
-
138
- export function archiveDisplayName(file: string): string {
139
- return basename(file);
140
- }