@themoltnet/pi-extension 0.8.0 → 0.9.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.
package/dist/index.d.ts CHANGED
@@ -281,6 +281,8 @@ declare const Task: TObject< {
281
281
  cancelledByHumanId: TUnion<[TString, TNull]>;
282
282
  cancelReason: TUnion<[TString, TNull]>;
283
283
  maxAttempts: TNumber;
284
+ dispatchTimeoutSec: TUnion<[TInteger, TNull]>;
285
+ runningTimeoutSec: TUnion<[TInteger, TNull]>;
284
286
  }>;
285
287
 
286
288
  declare type Task = Static<typeof Task>;
@@ -364,6 +366,26 @@ declare interface TaskReporter {
364
366
  finalize(usage: TaskUsage): Promise<void>;
365
367
  /** Flush buffers + release resources. Called once. Idempotent. */
366
368
  close(): Promise<void>;
369
+ /**
370
+ * Signal that aborts when the task is cancelled by the imposer (or a
371
+ * diary writer) while the executor is running. `ApiTaskReporter`
372
+ * aborts this on the next heartbeat that observes `cancelled: true`
373
+ * in the response (#938). Local reporters (`StdoutReporter`,
374
+ * `JsonlTaskReporter`) never abort — there's no remote cancel
375
+ * channel for `FileTaskSource`.
376
+ *
377
+ * Executors should pass this signal into long-running work
378
+ * (LLM calls, sandbox execution, file ops) and surface a
379
+ * `status: 'cancelled'` output when it fires. The runtime also
380
+ * checks the signal post-execute and converts any output to
381
+ * `cancelled` if the executor returned without honoring it.
382
+ */
383
+ readonly cancelSignal: AbortSignal;
384
+ /**
385
+ * The reason supplied to `/tasks/:id/cancel` by the canceller, if
386
+ * cancellation has been observed. Null until `cancelSignal` aborts.
387
+ */
388
+ readonly cancelReason: string | null;
367
389
  }
368
390
 
369
391
  /**
package/dist/index.js CHANGED
@@ -9338,7 +9338,15 @@ Type$1.Object({
9338
9338
  cancelledByAgentId: Type$1.Union([Uuid, Type$1.Null()]),
9339
9339
  cancelledByHumanId: Type$1.Union([Uuid, Type$1.Null()]),
9340
9340
  cancelReason: Type$1.Union([Type$1.String(), Type$1.Null()]),
9341
- maxAttempts: Type$1.Number({ minimum: 1 })
9341
+ maxAttempts: Type$1.Number({ minimum: 1 }),
9342
+ dispatchTimeoutSec: Type$1.Union([Type$1.Integer({
9343
+ minimum: 1,
9344
+ maximum: 86400
9345
+ }), Type$1.Null()]),
9346
+ runningTimeoutSec: Type$1.Union([Type$1.Integer({
9347
+ minimum: 1,
9348
+ maximum: 86400
9349
+ }), Type$1.Null()])
9342
9350
  }, {
9343
9351
  $id: "Task",
9344
9352
  additionalProperties: false
@@ -9499,11 +9507,7 @@ function buildAssessBriefPrompt(input, ctx) {
9499
9507
  */
9500
9508
  function buildCuratePackPrompt(input, ctx) {
9501
9509
  const { diaryId, taskPrompt, entryTypes, tagFilters, tokenBudget, recipe } = input;
9502
- const effectiveEntryTypes = entryTypes ?? [
9503
- "semantic",
9504
- "episodic",
9505
- "procedural"
9506
- ];
9510
+ const entryTypesPinned = Boolean(entryTypes);
9507
9511
  const resolvedRecipe = recipe ?? "topic-focused-v1";
9508
9512
  const includeLine = tagFilters?.include?.length ? `- Hard include (ALL must be present on an entry): ${tagFilters.include.map((t) => `\`${t}\``).join(", ")}` : null;
9509
9513
  const excludeLine = tagFilters?.exclude?.length ? `- Hard exclude (drop if ANY present): ${tagFilters.exclude.map((t) => `\`${t}\``).join(", ")}` : null;
@@ -9532,7 +9536,16 @@ function buildCuratePackPrompt(input, ctx) {
9532
9536
  "",
9533
9537
  "## Constraints",
9534
9538
  "",
9535
- `- Entry types in play: ${effectiveEntryTypes.map((t) => `\`${t}\``).join(", ")}`,
9539
+ entryTypesPinned ? `- Entry types pinned by imposer (do not widen): ${entryTypes.map((t) => `\`${t}\``).join(", ")}` : "- Entry types: **you choose**. The diary contains three kinds:",
9540
+ entryTypesPinned ? null : " - `episodic` — incident reports, \"what happened and how we fixed it\" narratives.",
9541
+ entryTypesPinned ? null : " - `semantic` — durable decisions, patterns, design rationale.",
9542
+ entryTypesPinned ? null : " - `procedural` — commit audit trails / changelog-style provenance.",
9543
+ entryTypesPinned ? null : " Pick the subset that fits the prompt. For \"failures and workarounds\"",
9544
+ entryTypesPinned ? null : " or \"decisions we made\" you generally do NOT want `procedural` — those",
9545
+ entryTypesPinned ? null : " entries are append-only commit logs and produce changelog-shaped packs.",
9546
+ entryTypesPinned ? null : " Include `procedural` only when the prompt explicitly asks for changelog-",
9547
+ entryTypesPinned ? null : " style content (e.g., \"what shipped this week\"). State your choice",
9548
+ entryTypesPinned ? null : " briefly in the final `summary`.",
9536
9549
  `- Recipe tag: \`${resolvedRecipe}\` (recorded on pack params)`,
9537
9550
  tokenBudget ? `- Token budget (soft cap on final pack): ${tokenBudget}. Pick entry count so the pack fits — estimate ~300 tok/entry as a starting heuristic, tighten after inspecting actual content lengths.` : "- No token budget — size the pack to match the prompt, not an arbitrary target.",
9538
9551
  includeLine,
@@ -10037,6 +10050,20 @@ async function executePiTask(claimedTask, reporter, opts) {
10037
10050
  const attemptN = claimedTask.attemptN;
10038
10051
  const startTime = Date.now();
10039
10052
  const mountPath = opts.mountPath ?? process.cwd();
10053
+ if (reporter.cancelSignal.aborted) return {
10054
+ taskId: task.id,
10055
+ attemptN,
10056
+ status: "cancelled",
10057
+ output: null,
10058
+ outputCid: null,
10059
+ usage: emptyUsage(opts.provider, opts.model),
10060
+ durationMs: Date.now() - startTime,
10061
+ error: {
10062
+ code: "task_cancelled",
10063
+ message: reporter.cancelReason ?? "Task cancelled before pi executor started.",
10064
+ retryable: false
10065
+ }
10066
+ };
10040
10067
  const checkpointPath = opts.checkpointPath ?? await ensureSnapshot({
10041
10068
  config: opts.sandboxConfig?.snapshot,
10042
10069
  onProgress: opts.onSnapshotProgress ?? ((m) => {
@@ -10065,6 +10092,7 @@ async function executePiTask(claimedTask, reporter, opts) {
10065
10092
  let reporterOpen = false;
10066
10093
  let session = null;
10067
10094
  const finalUsage = emptyUsage(opts.provider, opts.model);
10095
+ let cancelListener = null;
10068
10096
  const makeFailedOutput = (code, message, usage = finalUsage) => ({
10069
10097
  taskId: task.id,
10070
10098
  attemptN,
@@ -10165,6 +10193,7 @@ async function executePiTask(claimedTask, reporter, opts) {
10165
10193
  let assistantText = "";
10166
10194
  let reporterError = null;
10167
10195
  const usage = finalUsage;
10196
+ cancelListener = wireSessionAbort(reporter.cancelSignal, session);
10168
10197
  const recordingPromise = [];
10169
10198
  const track = (p) => {
10170
10199
  recordingPromise.push(p.catch((err) => {
@@ -10220,10 +10249,11 @@ async function executePiTask(claimedTask, reporter, opts) {
10220
10249
  });
10221
10250
  }
10222
10251
  await Promise.all(recordingPromise);
10252
+ const cancelled = reporter.cancelSignal.aborted;
10223
10253
  let parsedOutput = null;
10224
10254
  let parsedOutputCid = null;
10225
10255
  let parseError = null;
10226
- if (!runError && !llmAbort) {
10256
+ if (!runError && !llmAbort && !cancelled) {
10227
10257
  const parsed = await parseStructuredTaskOutput(assistantText, task.taskType);
10228
10258
  parsedOutput = parsed.output;
10229
10259
  parsedOutputCid = parsed.outputCid;
@@ -10233,6 +10263,20 @@ async function executePiTask(claimedTask, reporter, opts) {
10233
10263
  phase: "output_validation"
10234
10264
  });
10235
10265
  }
10266
+ if (cancelled) return {
10267
+ taskId: task.id,
10268
+ attemptN,
10269
+ status: "cancelled",
10270
+ output: null,
10271
+ outputCid: null,
10272
+ usage,
10273
+ durationMs: Date.now() - startTime,
10274
+ error: {
10275
+ code: "task_cancelled",
10276
+ message: reporter.cancelReason ?? "Task cancelled by imposer while pi session was running.",
10277
+ retryable: false
10278
+ }
10279
+ };
10236
10280
  const status = runError || llmAbort || parseError || reporterError ? "failed" : "completed";
10237
10281
  const errorCode = runError?.code ?? parseError?.code ?? reporterError?.code ?? (llmAbort ? "llm_api_error" : void 0);
10238
10282
  const errorMessage = runError?.message ?? parseError?.message ?? reporterError?.message ?? (llmAbort ? "LLM API error during turn" : void 0);
@@ -10253,6 +10297,7 @@ async function executePiTask(claimedTask, reporter, opts) {
10253
10297
  } catch (err) {
10254
10298
  return makeFailedOutput("executor_unexpected_error", err instanceof Error ? err.message : String(err));
10255
10299
  } finally {
10300
+ if (cancelListener) reporter.cancelSignal.removeEventListener("abort", cancelListener);
10256
10301
  if (session) try {
10257
10302
  session.dispose();
10258
10303
  } catch {}
@@ -10282,6 +10327,32 @@ function emptyUsage(provider, model) {
10282
10327
  };
10283
10328
  }
10284
10329
  /**
10330
+ * Wire `cancelSignal` → `session.abort()`. Returns the listener so the
10331
+ * caller can remove it on cleanup. If the signal is already aborted at
10332
+ * call time (cancel landed between session creation and wiring), fires
10333
+ * abort synchronously instead of waiting for an `'abort'` event that
10334
+ * already happened.
10335
+ *
10336
+ * Exported for unit testing without a booted Gondolin VM. The double-
10337
+ * invocation guard handles both the rare "fire from constructor + later
10338
+ * event" race and the (in-practice idempotent) double-call into
10339
+ * `session.abort()`.
10340
+ */
10341
+ function wireSessionAbort(cancelSignal, session) {
10342
+ let abortInvoked = false;
10343
+ const listener = () => {
10344
+ if (abortInvoked) return;
10345
+ abortInvoked = true;
10346
+ session.abort().catch((err) => {
10347
+ const message = err instanceof Error ? err.message : String(err);
10348
+ process.stderr.write(`[pi] session.abort() failed: ${message}\n`);
10349
+ });
10350
+ };
10351
+ if (cancelSignal.aborted) listener();
10352
+ else cancelSignal.addEventListener("abort", listener, { once: true });
10353
+ return listener;
10354
+ }
10355
+ /**
10285
10356
  * Cap oversized tool-result payloads before embedding them in a
10286
10357
  * `task_messages.payload` row. Bodies above 4 KiB are replaced with a
10287
10358
  * `{ truncated, original_size }` marker so the JSONL/DB size stays bounded.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "MoltNet pi extension — sandboxed tool execution in Gondolin VMs with MoltNet identity and persistent memory",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "@earendil-works/gondolin": "^0.7.0",
32
32
  "@opentelemetry/api": "^1.9.0",
33
33
  "@sinclair/typebox": "^0.34.0",
34
- "@themoltnet/agent-runtime": "0.4.0",
34
+ "@themoltnet/agent-runtime": "0.5.0",
35
35
  "@themoltnet/sdk": "0.96.0"
36
36
  },
37
37
  "peerDependencies": {