pi-outpost 0.27.1 → 0.28.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.
@@ -12370,7 +12370,7 @@ function rulePatterns(profile, rules, sources) {
12370
12370
  }
12371
12371
 
12372
12372
  // shared/bin/validate-structured-exchange.mjs
12373
- var VERSION = `validate-structured-exchange ${false ? "development" : "0.27.1"}`;
12373
+ var VERSION = `validate-structured-exchange ${false ? "development" : "0.28.0"}`;
12374
12374
  var USAGE = `validate-structured-exchange [file]
12375
12375
  validate-structured-exchange --profile <profile.json> [file]
12376
12376
  validate-structured-exchange --check-profile <profile.json>
@@ -132364,6 +132364,12 @@ var WORST_LISTED = 8;
132364
132364
  function isStackExhaustion(message) {
132365
132365
  return /maximum call stack size exceeded|call stack size exceeded|stack overflow/i.test(message);
132366
132366
  }
132367
+ var PROVIDER_STACK_ENV = "PI_OUTPOST_PROVIDER_STACK";
132368
+ var PROVIDER_STACK_OFF = /* @__PURE__ */ new Set(["", "0", "false", "no", "off"]);
132369
+ function providerStackProbeArmed(env2 = process.env) {
132370
+ return !PROVIDER_STACK_OFF.has((env2[PROVIDER_STACK_ENV] ?? "").trim().toLowerCase());
132371
+ }
132372
+ var PROVIDER_STACK_HINT = `[pi-outpost] no stack accompanies this \u2014 the provider's catch drops it. Set ${PROVIDER_STACK_ENV}=1 and the next occurrence will print one.`;
132367
132373
  function inspectShape(root) {
132368
132374
  let depth = 0;
132369
132375
  let cyclic = false;
@@ -132508,6 +132514,7 @@ function writeRecord(agentDir, report) {
132508
132514
  context?.cyclic?.length ? ` SELF-REFERENCE in ${context.cyclic.length} entr${context.cyclic.length === 1 ? "y" : "ies"}` : ""
132509
132515
  ].join("");
132510
132516
  console.error(headline);
132517
+ if (!providerStackProbeArmed()) console.error(PROVIDER_STACK_HINT);
132511
132518
  const file = path23.join(agentDir, "turn-failures.jsonl");
132512
132519
  try {
132513
132520
  fs21.mkdirSync(agentDir, { recursive: true });
@@ -143476,7 +143483,7 @@ function cacheControlFor(url) {
143476
143483
  }
143477
143484
 
143478
143485
  // ../server/src/index.ts
143479
- var VERSION2 = true ? "0.27.1" : "dev";
143486
+ var VERSION2 = true ? "0.28.0" : "dev";
143480
143487
  var PI_SDK_VERSION = true ? "0.86.1" : readInstalledPiSdkVersion();
143481
143488
  var LAUNCH_DIR = process.env.INIT_CWD ?? process.cwd();
143482
143489
  function complain(error) {
@@ -144212,8 +144219,8 @@ for (const root of config.openProjects) {
144212
144219
  }
144213
144220
  workspace.workPlan = await loadWorkPlan(workspace.agent.snapshot().sessionFile);
144214
144221
  workspace.workPlanSessionFile = workspace.agent.snapshot().sessionFile;
144215
- publishWorkPlanTools(workspace, workspace.workPlan);
144216
144222
  withholdDocumentTools(workspace);
144223
+ publishWorkPlanTools(workspace, workspace.workPlan);
144217
144224
  function withholdDocumentTools(workspace2) {
144218
144225
  for (const tool of [...DOCUMENT_TOOLS, PROJECT_MODEL_TOOL]) workspace2.agent.setToolPublished(tool, false);
144219
144226
  workspace2.documentToolIdleTurns.clear();
@@ -144241,11 +144248,12 @@ function unpublishDocumentTools(workspace2, tools) {
144241
144248
  }
144242
144249
  }
144243
144250
  var DOCUMENT_TOOL_IDLE_LIMIT = { unused: 1, used: 5 };
144251
+ var WORK_PLAN_EXTENDED_IDLE_LIMIT = 5;
144244
144252
  function ageDocumentTools(workspace2) {
144245
144253
  const used = workspace2.documentToolsUsedThisTurn;
144246
144254
  for (const [tool, idle] of workspace2.documentToolIdleTurns) {
144247
144255
  if (used.has(tool)) continue;
144248
- const limit = workspace2.documentToolsEverUsed.has(tool) ? DOCUMENT_TOOL_IDLE_LIMIT.used : DOCUMENT_TOOL_IDLE_LIMIT.unused;
144256
+ const limit = tool === WORK_PLAN_EXTENDED_TOOL ? WORK_PLAN_EXTENDED_IDLE_LIMIT : workspace2.documentToolsEverUsed.has(tool) ? DOCUMENT_TOOL_IDLE_LIMIT.used : DOCUMENT_TOOL_IDLE_LIMIT.unused;
144249
144257
  if (idle + 1 < limit) {
144250
144258
  workspace2.documentToolIdleTurns.set(tool, idle + 1);
144251
144259
  continue;
@@ -144256,7 +144264,10 @@ function ageDocumentTools(workspace2) {
144256
144264
  used.clear();
144257
144265
  }
144258
144266
  function publishWorkPlanTools(workspace2, plan) {
144259
- workspace2.agent.setToolPublished(WORK_PLAN_EXTENDED_TOOL, plan !== null);
144267
+ const published = plan !== null;
144268
+ if (!workspace2.agent.setToolPublished(WORK_PLAN_EXTENDED_TOOL, published)) return;
144269
+ if (published) workspace2.documentToolIdleTurns.set(WORK_PLAN_EXTENDED_TOOL, 0);
144270
+ else workspace2.documentToolIdleTurns.delete(WORK_PLAN_EXTENDED_TOOL);
144260
144271
  }
144261
144272
  function queueWorkPlanSessionSync(workspace2, after) {
144262
144273
  const sessionFile = workspace2.agent.snapshot().sessionFile;
@@ -144276,8 +144287,8 @@ function queueWorkPlanSessionSync(workspace2, after) {
144276
144287
  if (!sameSessionFile(workspace2.agent.snapshot().sessionFile, sessionFile)) return;
144277
144288
  workspace2.workPlan = plan;
144278
144289
  workspace2.workPlanSessionFile = sessionFile;
144279
- publishWorkPlanTools(workspace2, plan);
144280
144290
  withholdDocumentTools(workspace2);
144291
+ publishWorkPlanTools(workspace2, plan);
144281
144292
  broadcast(workspace2, { type: "session_replaced", ...snapshot(workspace2) });
144282
144293
  if (inherited) broadcast(workspace2, { type: "work_plan_changed", workPlan: plan });
144283
144294
  announceWorkspaceActivity();
@@ -145251,8 +145262,8 @@ async function ensureStarted(target) {
145251
145262
  refreshExtensionRender(target);
145252
145263
  target.workPlan = await loadWorkPlan(runtime.snapshot().sessionFile);
145253
145264
  target.workPlanSessionFile = runtime.snapshot().sessionFile;
145254
- publishWorkPlanTools(target, target.workPlan);
145255
145265
  withholdDocumentTools(target);
145266
+ publishWorkPlanTools(target, target.workPlan);
145256
145267
  target.lastUsedAt = Date.now();
145257
145268
  void refreshPiPackages(target);
145258
145269
  })();
@@ -468768,6 +468768,12 @@ var WORST_LISTED = 8;
468768
468768
  function isStackExhaustion(message) {
468769
468769
  return /maximum call stack size exceeded|call stack size exceeded|stack overflow/i.test(message);
468770
468770
  }
468771
+ var PROVIDER_STACK_ENV = "PI_OUTPOST_PROVIDER_STACK";
468772
+ var PROVIDER_STACK_OFF = /* @__PURE__ */ new Set(["", "0", "false", "no", "off"]);
468773
+ function providerStackProbeArmed(env4 = process.env) {
468774
+ return !PROVIDER_STACK_OFF.has((env4[PROVIDER_STACK_ENV] ?? "").trim().toLowerCase());
468775
+ }
468776
+ var PROVIDER_STACK_HINT = `[pi-outpost] no stack accompanies this \u2014 the provider's catch drops it. Set ${PROVIDER_STACK_ENV}=1 and the next occurrence will print one.`;
468771
468777
  function inspectShape(root) {
468772
468778
  let depth = 0;
468773
468779
  let cyclic = false;
@@ -468912,6 +468918,7 @@ function writeRecord(agentDir, report) {
468912
468918
  context?.cyclic?.length ? ` SELF-REFERENCE in ${context.cyclic.length} entr${context.cyclic.length === 1 ? "y" : "ies"}` : ""
468913
468919
  ].join("");
468914
468920
  console.error(headline);
468921
+ if (!providerStackProbeArmed()) console.error(PROVIDER_STACK_HINT);
468915
468922
  const file = path41.join(agentDir, "turn-failures.jsonl");
468916
468923
  try {
468917
468924
  fs35.mkdirSync(agentDir, { recursive: true });
@@ -471379,7 +471386,7 @@ function cacheControlFor(url) {
471379
471386
  }
471380
471387
 
471381
471388
  // ../server/src/index.ts
471382
- var VERSION4 = true ? "0.27.1" : "dev";
471389
+ var VERSION4 = true ? "0.28.0" : "dev";
471383
471390
  var PI_SDK_VERSION = true ? "0.86.1" : readInstalledPiSdkVersion();
471384
471391
  var LAUNCH_DIR = process.env.INIT_CWD ?? process.cwd();
471385
471392
  function complain(error) {
@@ -472115,8 +472122,8 @@ for (const root of config.openProjects) {
472115
472122
  }
472116
472123
  workspace.workPlan = await loadWorkPlan(workspace.agent.snapshot().sessionFile);
472117
472124
  workspace.workPlanSessionFile = workspace.agent.snapshot().sessionFile;
472118
- publishWorkPlanTools(workspace, workspace.workPlan);
472119
472125
  withholdDocumentTools(workspace);
472126
+ publishWorkPlanTools(workspace, workspace.workPlan);
472120
472127
  function withholdDocumentTools(workspace2) {
472121
472128
  for (const tool of [...DOCUMENT_TOOLS, PROJECT_MODEL_TOOL]) workspace2.agent.setToolPublished(tool, false);
472122
472129
  workspace2.documentToolIdleTurns.clear();
@@ -472144,11 +472151,12 @@ function unpublishDocumentTools(workspace2, tools) {
472144
472151
  }
472145
472152
  }
472146
472153
  var DOCUMENT_TOOL_IDLE_LIMIT = { unused: 1, used: 5 };
472154
+ var WORK_PLAN_EXTENDED_IDLE_LIMIT = 5;
472147
472155
  function ageDocumentTools(workspace2) {
472148
472156
  const used = workspace2.documentToolsUsedThisTurn;
472149
472157
  for (const [tool, idle] of workspace2.documentToolIdleTurns) {
472150
472158
  if (used.has(tool)) continue;
472151
- const limit3 = workspace2.documentToolsEverUsed.has(tool) ? DOCUMENT_TOOL_IDLE_LIMIT.used : DOCUMENT_TOOL_IDLE_LIMIT.unused;
472159
+ const limit3 = tool === WORK_PLAN_EXTENDED_TOOL ? WORK_PLAN_EXTENDED_IDLE_LIMIT : workspace2.documentToolsEverUsed.has(tool) ? DOCUMENT_TOOL_IDLE_LIMIT.used : DOCUMENT_TOOL_IDLE_LIMIT.unused;
472152
472160
  if (idle + 1 < limit3) {
472153
472161
  workspace2.documentToolIdleTurns.set(tool, idle + 1);
472154
472162
  continue;
@@ -472159,7 +472167,10 @@ function ageDocumentTools(workspace2) {
472159
472167
  used.clear();
472160
472168
  }
472161
472169
  function publishWorkPlanTools(workspace2, plan) {
472162
- workspace2.agent.setToolPublished(WORK_PLAN_EXTENDED_TOOL, plan !== null);
472170
+ const published = plan !== null;
472171
+ if (!workspace2.agent.setToolPublished(WORK_PLAN_EXTENDED_TOOL, published)) return;
472172
+ if (published) workspace2.documentToolIdleTurns.set(WORK_PLAN_EXTENDED_TOOL, 0);
472173
+ else workspace2.documentToolIdleTurns.delete(WORK_PLAN_EXTENDED_TOOL);
472163
472174
  }
472164
472175
  function queueWorkPlanSessionSync(workspace2, after) {
472165
472176
  const sessionFile = workspace2.agent.snapshot().sessionFile;
@@ -472179,8 +472190,8 @@ function queueWorkPlanSessionSync(workspace2, after) {
472179
472190
  if (!sameSessionFile(workspace2.agent.snapshot().sessionFile, sessionFile)) return;
472180
472191
  workspace2.workPlan = plan;
472181
472192
  workspace2.workPlanSessionFile = sessionFile;
472182
- publishWorkPlanTools(workspace2, plan);
472183
472193
  withholdDocumentTools(workspace2);
472194
+ publishWorkPlanTools(workspace2, plan);
472184
472195
  broadcast(workspace2, { type: "session_replaced", ...snapshot(workspace2) });
472185
472196
  if (inherited) broadcast(workspace2, { type: "work_plan_changed", workPlan: plan });
472186
472197
  announceWorkspaceActivity();
@@ -473154,8 +473165,8 @@ async function ensureStarted(target) {
473154
473165
  refreshExtensionRender(target);
473155
473166
  target.workPlan = await loadWorkPlan(runtime.snapshot().sessionFile);
473156
473167
  target.workPlanSessionFile = runtime.snapshot().sessionFile;
473157
- publishWorkPlanTools(target, target.workPlan);
473158
473168
  withholdDocumentTools(target);
473169
+ publishWorkPlanTools(target, target.workPlan);
473159
473170
  target.lastUsedAt = Date.now();
473160
473171
  void refreshPiPackages(target);
473161
473172
  })();
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Arm the provider stack probe, or do nothing at all.
3
+ *
4
+ * Passed with `--import`, because the whole module graph is loaded before any of
5
+ * pi-outpost's own code evaluates: a `register()` call from inside `index.ts` would run
6
+ * after the SDK was already parsed, and rewrite nothing. This is the only slot early
7
+ * enough, which is why it is a module of its own rather than a few lines somewhere.
8
+ *
9
+ * Inert unless `PI_OUTPOST_PROVIDER_STACK` is set to something meaning yes. Off, this
10
+ * file costs one environment read and returns; nothing is registered, no module is
11
+ * rewritten, and the process runs exactly as it did before. That is the point of the
12
+ * flag: a diagnostic nobody asked for should not be in the path of everybody's turns.
13
+ *
14
+ * On, it also raises `Error.stackTraceLimit`. V8's default of 10 frames is nothing on a
15
+ * recursion — the repeated frame is the answer, and ten of it says only that something
16
+ * repeated.
17
+ */
18
+ import { register } from "node:module";
19
+ import { MessageChannel } from "node:worker_threads";
20
+
21
+ export const ENV_VAR = "PI_OUTPOST_PROVIDER_STACK";
22
+
23
+ /** How many frames are worth keeping. A recursion shows its cycle long before this. */
24
+ export const STACK_TRACE_LIMIT = 200;
25
+
26
+ /** Whether the flag's value means yes. Unset, empty, `0`, `false` and `no` mean no. */
27
+ export function isEnabled(value) {
28
+ if (typeof value !== "string") return false;
29
+ const normalized = value.trim().toLowerCase();
30
+ if (normalized.length === 0) return false;
31
+ return normalized !== "0" && normalized !== "false" && normalized !== "no" && normalized !== "off";
32
+ }
33
+
34
+ /**
35
+ * Registers the load hook and returns the port arming announcements arrive on.
36
+ *
37
+ * Exported so a test can drive it without a second process, and so the caller decides
38
+ * what to do with the announcements — this module only prints them.
39
+ */
40
+ export function armProviderStackProbe(env = process.env) {
41
+ if (!isEnabled(env[ENV_VAR])) return undefined;
42
+ Error.stackTraceLimit = STACK_TRACE_LIMIT;
43
+ const { port1, port2 } = new MessageChannel();
44
+ port1.on("message", (url) => {
45
+ console.error(`[pi-outpost] provider stack probe armed on ${url}`);
46
+ });
47
+ // Nothing should wait on this port: an armed probe must not hold the event loop open.
48
+ port1.unref();
49
+ register("./providerStackRewrite.mjs", {
50
+ parentURL: import.meta.url,
51
+ data: port2,
52
+ transferList: [port2],
53
+ });
54
+ console.error(
55
+ `[pi-outpost] provider stack probe on (${ENV_VAR}). If no probe reports in by the first` +
56
+ " model call, the SDK's catch has moved and there was nothing to read.",
57
+ );
58
+ return port1;
59
+ }
60
+
61
+ armProviderStackProbe();
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The module-load hook that puts a provider's lost stack back on stderr.
3
+ *
4
+ * Every pi-ai provider's stream loop ends in the same catch:
5
+ *
6
+ * catch (error) { output.errorMessage = formatProviderError(normalizeProviderError(error)); … }
7
+ *
8
+ * The message is kept and the `Error` is dropped, so by the time `assistant_end`
9
+ * crosses the runtime seam there is no `.stack` left anywhere in the process — which is
10
+ * the gap `turnFailureLog.ts` was written around, and why it records a census of the
11
+ * *input* rather than a trace. `normalizeProviderError` returns a flat
12
+ * `{ status, body, message, messageCarriesBody }`; it never carries the stack either.
13
+ * No extension hook reaches this: `after_provider_response` fires before the stream is
14
+ * consumed, and V8 offers no hook at an `Error`'s construction — `prepareStackTrace`
15
+ * runs on the first read of `.stack`, which never happens.
16
+ *
17
+ * So the stack is read here, at module load, before the catch can drop it. The source
18
+ * is rewritten **in memory**: nothing under `node_modules` is touched, `npm ci` has
19
+ * nothing to undo, and a machine running without the flag runs the bytes it installed.
20
+ *
21
+ * Matched on the catch's own line rather than on a filename, so it arms every provider
22
+ * that shares it — and arms none if a future SDK rewrites that line. That silence is
23
+ * reported rather than left to be discovered: each arming is announced through the
24
+ * port, and a run that announces nothing is a hook that found nothing.
25
+ *
26
+ * Hooks run off-thread, so `console.error` from here would not interleave with the
27
+ * server's own output; the port is how this side talks back. The injected probe itself
28
+ * runs in the module's own context on the main thread, where stderr is the real one.
29
+ */
30
+
31
+ /** The assignment that drops the error, verbatim from every pi-ai provider. */
32
+ const NEEDLE = "output.errorMessage = formatProviderError(normalizeProviderError(error));";
33
+
34
+ /**
35
+ * Only stack exhaustion. Deliberately the same test as `isStackExhaustion` in
36
+ * `turnFailureLog.ts`: every other provider failure is already legible in the red
37
+ * bubble, and dumping 200 frames for a rate limit would bury the rare event in the
38
+ * common ones.
39
+ */
40
+ const PROBE = [
41
+ "try { if (/maximum call stack size exceeded|call stack size exceeded|stack overflow/i.test(String(error && error.message)))",
42
+ ' console.error("[pi-outpost] provider stack:\\n" + ((error && error.stack) || String(error)));',
43
+ " } catch { }",
44
+ ].join("");
45
+
46
+ /** Set by `initialize`, which `module.register` calls with the port it was given. */
47
+ let port;
48
+
49
+ export function initialize(data) {
50
+ port = data;
51
+ }
52
+
53
+ export async function load(url, context, nextLoad) {
54
+ const result = await nextLoad(url, context);
55
+ if (result.source === undefined || result.source === null) return result;
56
+ const source = result.source.toString();
57
+ if (!source.includes(NEEDLE)) return result;
58
+ port?.postMessage(url);
59
+ // Prepended, not replacing: the assignment still runs and the turn still fails the
60
+ // way it did. This hook observes, it does not change what the agent sees.
61
+ return { ...result, source: source.replaceAll(NEEDLE, `${PROBE} ${NEEDLE}`) };
62
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-outpost",
3
- "version": "0.27.1",
3
+ "version": "0.28.0",
4
4
  "type": "module",
5
5
  "description": "A web interface for the pi coding agent: a browser chat UI you run with npx — no clone, no build.",
6
6
  "license": "MIT",