pi-daddy 0.48.2 → 0.49.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 (87) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +74 -8
  3. package/dist/cli.d.ts +3 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +18 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/executors/captured-execution.d.ts.map +1 -1
  8. package/dist/executors/captured-execution.js +12 -1
  9. package/dist/executors/captured-execution.js.map +1 -1
  10. package/dist/executors/herdr-launcher.d.ts.map +1 -1
  11. package/dist/executors/herdr-launcher.js +22 -3
  12. package/dist/executors/herdr-launcher.js.map +1 -1
  13. package/dist/executors/owned-pi-ui.d.ts +3 -0
  14. package/dist/executors/owned-pi-ui.d.ts.map +1 -0
  15. package/dist/executors/owned-pi-ui.js +114 -0
  16. package/dist/executors/owned-pi-ui.js.map +1 -0
  17. package/dist/executors/owned-pi-view.d.ts +5 -0
  18. package/dist/executors/owned-pi-view.d.ts.map +1 -0
  19. package/dist/executors/owned-pi-view.js +60 -0
  20. package/dist/executors/owned-pi-view.js.map +1 -0
  21. package/dist/executors/owned-worker.d.ts +2 -0
  22. package/dist/executors/owned-worker.d.ts.map +1 -1
  23. package/dist/executors/owned-worker.js +27 -11
  24. package/dist/executors/owned-worker.js.map +1 -1
  25. package/dist/governance/approval-prompt.d.ts +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/kernel/selected-definitions.d.ts.map +1 -1
  31. package/dist/kernel/selected-definitions.js +6 -3
  32. package/dist/kernel/selected-definitions.js.map +1 -1
  33. package/dist/products/dashboard-cli.d.ts.map +1 -1
  34. package/dist/products/dashboard-cli.js +46 -1
  35. package/dist/products/dashboard-cli.js.map +1 -1
  36. package/dist/products/dashboard-display-controls.d.ts +1 -1
  37. package/dist/products/dashboard-display-controls.d.ts.map +1 -1
  38. package/dist/products/dashboard-display-controls.js +11 -3
  39. package/dist/products/dashboard-display-controls.js.map +1 -1
  40. package/dist/products/dashboard-screen.d.ts +3 -1
  41. package/dist/products/dashboard-screen.d.ts.map +1 -1
  42. package/dist/products/dashboard-screen.js +21 -8
  43. package/dist/products/dashboard-screen.js.map +1 -1
  44. package/dist/products/dashboard-session-client.d.ts +9 -0
  45. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  46. package/dist/products/dashboard-session-client.js.map +1 -1
  47. package/dist/products/dashboard-settings.d.ts +28 -0
  48. package/dist/products/dashboard-settings.d.ts.map +1 -0
  49. package/dist/products/dashboard-settings.js +42 -0
  50. package/dist/products/dashboard-settings.js.map +1 -0
  51. package/dist/products/diagnostic-export.d.ts +23 -0
  52. package/dist/products/diagnostic-export.d.ts.map +1 -0
  53. package/dist/products/diagnostic-export.js +264 -0
  54. package/dist/products/diagnostic-export.js.map +1 -0
  55. package/dist/products/diagnostic-filter.d.ts +7 -0
  56. package/dist/products/diagnostic-filter.d.ts.map +1 -0
  57. package/dist/products/diagnostic-filter.js +135 -0
  58. package/dist/products/diagnostic-filter.js.map +1 -0
  59. package/extensions/approvals.ts +3 -2
  60. package/extensions/dashboard-session-server.ts +17 -2
  61. package/extensions/dashboard-settings.ts +149 -0
  62. package/extensions/delegation-operations.ts +81 -2
  63. package/extensions/execute-child.ts +10 -11
  64. package/extensions/execution-controls.ts +7 -2
  65. package/extensions/grants-command.ts +3 -1
  66. package/extensions/grants.ts +3 -0
  67. package/extensions/reload-environment.ts +3 -0
  68. package/extensions/run-delegation.ts +3 -1
  69. package/extensions/runtime-snapshot.ts +9 -1
  70. package/extensions/session.ts +3 -8
  71. package/package.json +1 -1
  72. package/src/cli.ts +24 -1
  73. package/src/executors/captured-execution.ts +13 -1
  74. package/src/executors/herdr-launcher.ts +15 -2
  75. package/src/executors/owned-pi-ui.ts +116 -0
  76. package/src/executors/owned-pi-view.ts +58 -0
  77. package/src/executors/owned-worker.ts +29 -11
  78. package/src/governance/approval-prompt.ts +1 -1
  79. package/src/index.ts +2 -0
  80. package/src/kernel/selected-definitions.ts +5 -3
  81. package/src/products/dashboard-cli.ts +38 -1
  82. package/src/products/dashboard-display-controls.ts +10 -4
  83. package/src/products/dashboard-screen.ts +21 -10
  84. package/src/products/dashboard-session-client.ts +7 -1
  85. package/src/products/dashboard-settings.ts +72 -0
  86. package/src/products/diagnostic-export.ts +288 -0
  87. package/src/products/diagnostic-filter.ts +140 -0
@@ -0,0 +1,7 @@
1
+ export interface DiagnosticFilterResult {
2
+ bytes: Buffer;
3
+ omissions: Record<string, number>;
4
+ }
5
+ export type DiagnosticFormat = "session" | "json" | "jsonl" | "text";
6
+ export declare function filterDiagnostic(bytes: Uint8Array, format: DiagnosticFormat): DiagnosticFilterResult;
7
+ //# sourceMappingURL=diagnostic-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostic-filter.d.ts","sourceRoot":"","sources":["../../src/products/diagnostic-filter.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AACD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAErE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,GAAG,sBAAsB,CA0HpG"}
@@ -0,0 +1,135 @@
1
+ /** Local diagnostic projection. Known private fields are omitted; free-text secret detection is heuristic. */
2
+ import { parseRetentionJson } from "../governance/retention-json.js";
3
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
4
+ const privateKey = new RegExp("^(?:thinking|reasoning|reasoning_content|reasoning_details|encrypted_content|thoughtSignature|thinkingSignature|signature|" +
5
+ "env|environment|environ|credentials?|authorization|api[-_]?key|token|access[-_]?token|refresh[-_]?token|password|secret)$", "i");
6
+ const privateType = /^(?:thinking|reasoning|redacted_thinking|reasoning_content|reasoning_details)$/i;
7
+ export function filterDiagnostic(bytes, format) {
8
+ const omissions = {};
9
+ const note = (reason) => (omissions[reason] = (omissions[reason] ?? 0) + 1);
10
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
11
+ const redact = (input) => {
12
+ let output = input.replace(/\b(?:sk-(?:or-v1-)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g, () => {
13
+ note("credential-pattern");
14
+ return "[REDACTED]";
15
+ });
16
+ output = output.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/<>"']+@/gi, (_match, scheme) => {
17
+ note("credential-url-userinfo");
18
+ return `${scheme}[REDACTED]@`;
19
+ });
20
+ output = output.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, () => {
21
+ note("credential-pattern");
22
+ return "Bearer [REDACTED]";
23
+ });
24
+ output = output.replace(/\b([A-Z][A-Z0-9_]*(?:API_KEY|TOKEN|PASSWORD|SECRET)|API_KEY|TOKEN|PASSWORD|SECRET)(\s*[=:]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;]+)/gi, (_match, key, separator) => {
25
+ note("credential-assignment");
26
+ return `${key}${separator}[REDACTED]`;
27
+ });
28
+ return output;
29
+ };
30
+ const clean = (value, depth = 0) => {
31
+ if (depth > 64)
32
+ throw new Error("diagnostic nesting exceeds bounds");
33
+ if (typeof value === "string") {
34
+ // JSON embedded in a packet is a byte-bearing string. Keep it exactly when no redaction is needed.
35
+ if (/^\s*[\[{]/.test(value)) {
36
+ let parsed;
37
+ try {
38
+ parsed = parseRetentionJson(value, 16 * 1024 * 1024);
39
+ }
40
+ catch {
41
+ return redact(value);
42
+ }
43
+ const filtered = clean(parsed, depth + 1);
44
+ return JSON.stringify(filtered) === JSON.stringify(parsed) ? value : JSON.stringify(filtered);
45
+ }
46
+ return redact(value);
47
+ }
48
+ if (Array.isArray(value))
49
+ return value.flatMap((item) => {
50
+ if (object(item) && typeof item.type === "string" && privateType.test(item.type)) {
51
+ note("private-reasoning");
52
+ return [];
53
+ }
54
+ return [clean(item, depth + 1)];
55
+ });
56
+ if (!object(value))
57
+ return value;
58
+ if (typeof value.type === "string" && privateType.test(value.type)) {
59
+ note("private-reasoning");
60
+ return null;
61
+ }
62
+ const result = Object.create(null);
63
+ for (const [key, item] of Object.entries(value)) {
64
+ if (privateKey.test(key) || /(?:^|_)(?:API_KEY|ACCESS_TOKEN|REFRESH_TOKEN|PASSWORD|SECRET)$/.test(key)) {
65
+ note("private-field");
66
+ continue;
67
+ }
68
+ result[key] = clean(item, depth + 1);
69
+ }
70
+ return result;
71
+ };
72
+ const pick = (value, keys) => Object.fromEntries(keys.filter((key) => Object.hasOwn(value, key)).map((key) => [key, value[key]]));
73
+ const session = (value) => {
74
+ if (!object(value))
75
+ throw new Error("invalid native session record");
76
+ const envelope = pick(value, ["type", "id", "parentId", "timestamp"]);
77
+ if (value.type === "session")
78
+ return clean({ ...envelope, ...pick(value, ["version", "cwd", "parentSession"]) });
79
+ if (value.type === "message" && object(value.message)) {
80
+ const message = value.message;
81
+ if (!["user", "assistant", "toolResult"].includes(String(message.role))) {
82
+ note("unsupported-message");
83
+ return { ...envelope, omitted: "unsupported-message" };
84
+ }
85
+ const content = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content;
86
+ if (!Array.isArray(content))
87
+ throw new Error("invalid native message content");
88
+ const visible = content.flatMap((block) => {
89
+ if (!object(block))
90
+ throw new Error("invalid native content block");
91
+ if (block.type === "text" && typeof block.text === "string")
92
+ return [pick(block, ["type", "text"])];
93
+ if (block.type === "toolCall")
94
+ return [pick(block, ["type", "id", "name", "arguments"])];
95
+ note(typeof block.type === "string" && privateType.test(block.type) ? "private-reasoning" : "non-text-content");
96
+ return [];
97
+ });
98
+ return clean({
99
+ ...envelope,
100
+ message: {
101
+ ...pick(message, ["role", "toolCallId", "toolName", "isError", "provider", "model", "stopReason", "usage"]),
102
+ content: visible,
103
+ },
104
+ });
105
+ }
106
+ if (["model_change", "thinking_level_change", "session_info"].includes(String(value.type)))
107
+ return clean({ ...envelope, ...pick(value, ["provider", "modelId", "thinkingLevel", "name"]) });
108
+ note("unsupported-session-entry");
109
+ return clean({ ...envelope, omitted: "unsupported-session-entry" });
110
+ };
111
+ let output;
112
+ if (format === "text")
113
+ output = redact(text);
114
+ else if (format === "json")
115
+ output = JSON.stringify(clean(parseRetentionJson(text, 16 * 1024 * 1024))) + "\n";
116
+ else {
117
+ const lines = text.split("\n");
118
+ if (lines.at(-1) === "")
119
+ lines.pop();
120
+ if (format === "session") {
121
+ const header = parseRetentionJson(lines[0] ?? "", 16 * 1024 * 1024);
122
+ if (!object(header) || header.type !== "session" || header.version !== 3 || typeof header.id !== "string")
123
+ throw new Error("expected native Pi session v3");
124
+ }
125
+ output =
126
+ lines
127
+ .map((line) => {
128
+ const value = parseRetentionJson(line, 16 * 1024 * 1024);
129
+ return JSON.stringify(format === "session" ? session(value) : clean(value));
130
+ })
131
+ .join("\n") + "\n";
132
+ }
133
+ return { bytes: Buffer.from(output), omissions };
134
+ }
135
+ //# sourceMappingURL=diagnostic-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostic-filter.js","sourceRoot":"","sources":["../../src/products/diagnostic-filter.ts"],"names":[],"mappings":"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"}
@@ -233,8 +233,9 @@ export async function obtainApprovals(
233
233
  // precisely what spec §6.1 exists to prevent. Options are per-call; the queue is session-long.
234
234
  const gate = session.approvalGateFor({
235
235
  ui: ctx.ui,
236
- hasUI: ctx.hasUI,
237
- mode: ctx.mode,
236
+ // A native child tab is a viewer. Auto and inherited approvals still work; only the root can answer.
237
+ hasUI: ctx.hasUI && !(session.depth > 0),
238
+ mode: session.depth > 0 && ctx.mode === "tui" ? "view-only child" : ctx.mode,
238
239
  timeoutMs: () => timeoutMsFromEnv(process.env[ENV_APPROVAL_TIMEOUT]),
239
240
  autoMode: session.autoMode,
240
241
  });
@@ -1,3 +1,4 @@
1
+ import { dashboardSettingsSnapshot, dashboardJev, setDashboardLimit } from "./dashboard-settings.ts";
1
2
  import { randomBytes } from "node:crypto";
2
3
  import { chmod, rm } from "node:fs/promises";
3
4
  import { createServer, type Server } from "node:net";
@@ -69,9 +70,22 @@ async function start(session: GrantsSession): Promise<DashboardSessionEndpoint>
69
70
  }
70
71
 
71
72
  async function respond(session: GrantsSession, token: string, line: string): Promise<Record<string, unknown>> {
72
- const request = JSON.parse(line) as { token?: unknown; action?: unknown; edits?: unknown; enabled?: unknown };
73
+ const request = JSON.parse(line) as {
74
+ token?: unknown;
75
+ action?: unknown;
76
+ edits?: unknown;
77
+ enabled?: unknown;
78
+ key?: unknown;
79
+ seconds?: unknown;
80
+ };
73
81
  if (request.token !== token) return { ok: false, error: "unauthorised dashboard request" };
74
- if (request.action === "set-auto") {
82
+ let jev;
83
+ if (request.action === "set-limit") {
84
+ setDashboardLimit(session, request.key, request.seconds);
85
+ } else if (request.action === "set-jev") {
86
+ if (typeof request.enabled !== "boolean") return { ok: false, error: "JEV requires an explicit boolean" };
87
+ jev = await dashboardJev(session, request.enabled ? "enable" : "disable");
88
+ } else if (request.action === "set-auto") {
75
89
  if (typeof request.enabled !== "boolean") return { ok: false, error: "Auto mode requires an explicit boolean" };
76
90
  await setAutoApproval(session, request.enabled, "dashboard");
77
91
  } else if (request.action === "set") {
@@ -91,6 +105,7 @@ async function respond(session: GrantsSession, token: string, line: string): Pro
91
105
  ...(session.activity ? { activity: { ...session.activity } } : {}),
92
106
  rows: sessionModelRows(session, [...session.definitions.keys()].sort()),
93
107
  versions: versionSnapshot(session),
108
+ settings: await dashboardSettingsSnapshot(session, jev),
94
109
  cost: null, // Live complete episode cost is unavailable; retained usage remains in the episode report.
95
110
  };
96
111
  }
@@ -0,0 +1,149 @@
1
+ /** Owner-bound dashboard settings. No provider call, global env edit, or permission inference. */
2
+ import { randomUUID } from "node:crypto";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import type { GrantsSession } from "./session.ts";
5
+ import type { DashboardJevState, DashboardSettingsSnapshot } from "../src/products/dashboard-settings.ts";
6
+ import { MAX_CHILDREN_PER_CALL } from "../src/kernel/fanout.ts";
7
+ import {
8
+ ENV_CHILD_TIMEOUT,
9
+ ENV_CHILD_IDLE_TIMEOUT,
10
+ timeoutFromEnv,
11
+ idleTimeoutFromEnv,
12
+ } from "../src/kernel/run-child.ts";
13
+
14
+ const bridges = new WeakMap<GrantsSession, Pick<ExtensionAPI, "events">>();
15
+ const unavailable = (error?: string): DashboardJevState => ({
16
+ available: false,
17
+ enabled: false,
18
+ pending: false,
19
+ ...(error ? { error } : {}),
20
+ });
21
+ export function registerDashboardSettings(pi: Pick<ExtensionAPI, "events">, session: GrantsSession): void {
22
+ bridges.set(session, pi);
23
+ }
24
+ function owner(session: GrantsSession, edit = false) {
25
+ const current = session.reloadLifecycle?.autoMode;
26
+ if (!session.ownerBound || !current || current.reader !== session.autoMode)
27
+ throw new Error("Session settings owner is unavailable");
28
+ if (edit && !current.authority) throw new Error("Only the owning root session may edit settings");
29
+ return current;
30
+ }
31
+ export function childExecutionLimits(session: GrantsSession, env: NodeJS.ProcessEnv = process.env) {
32
+ const configured = session.reloadLifecycle?.executionSettings;
33
+ const current = session.reloadLifecycle?.autoMode;
34
+ const selected = configured?.nativeSessionId === current?.nativeSessionId ? configured : undefined;
35
+ return Object.freeze({
36
+ wallMs: selected?.wallMs ?? timeoutFromEnv(env[ENV_CHILD_TIMEOUT]),
37
+ idleMs: selected?.idleMs ?? idleTimeoutFromEnv(env[ENV_CHILD_IDLE_TIMEOUT]),
38
+ wallSource: selected?.wallMs === undefined ? ("startup" as const) : ("session" as const),
39
+ idleSource: selected?.idleMs === undefined ? ("startup" as const) : ("session" as const),
40
+ });
41
+ }
42
+ export function setDashboardLimit(session: GrantsSession, key: unknown, seconds: unknown): void {
43
+ const current = owner(session, true);
44
+ if (key !== "wall" && key !== "idle") throw new Error("Only child wall and idle limits are editable in this session");
45
+ if (
46
+ typeof seconds !== "number" ||
47
+ !Number.isSafeInteger(seconds) ||
48
+ seconds < 0 ||
49
+ seconds > Math.floor(2_147_483_647 / 1000)
50
+ )
51
+ throw new Error("Child limit must be whole seconds from 0 to 2147483; 0 restores the default");
52
+ const existing = session.reloadLifecycle.executionSettings;
53
+ const settings =
54
+ existing?.nativeSessionId === current.nativeSessionId
55
+ ? { ...existing }
56
+ : { nativeSessionId: current.nativeSessionId };
57
+ const ms = key === "wall" ? timeoutFromEnv(String(seconds)) : idleTimeoutFromEnv(String(seconds));
58
+ session.reloadLifecycle.executionSettings = { ...settings, [key === "wall" ? "wallMs" : "idleMs"]: ms };
59
+ }
60
+ const safeText = (value: unknown) => (typeof value === "string" ? value.slice(0, 120) : undefined);
61
+ export async function dashboardJev(
62
+ session: GrantsSession,
63
+ action: "status" | "enable" | "disable",
64
+ ): Promise<DashboardJevState> {
65
+ const current = owner(session, action !== "status");
66
+ const pi = bridges.get(session);
67
+ if (!pi?.events?.emit) {
68
+ if (action !== "status") throw new Error("JEV bridge unavailable; load skill-harness in the owner Pi session");
69
+ return unavailable();
70
+ }
71
+ const requestId = randomUUID();
72
+ return new Promise((resolve, reject) => {
73
+ const timer = setTimeout(
74
+ () => (action === "status" ? resolve(unavailable()) : reject(new Error("JEV control was not acknowledged"))),
75
+ 200,
76
+ );
77
+ const reply = (input: unknown) => {
78
+ const value = input as {
79
+ version?: number;
80
+ requestId?: string;
81
+ sessionId?: string;
82
+ pending?: boolean;
83
+ error?: string;
84
+ status?: {
85
+ enabled?: boolean;
86
+ mode?: string;
87
+ storage?: string | null;
88
+ remaining?: number;
89
+ availability?: string;
90
+ providerReadiness?: string;
91
+ };
92
+ };
93
+ if (!value || value.version !== 1 || value.requestId !== requestId || value.sessionId !== current.nativeSessionId)
94
+ return;
95
+ clearTimeout(timer);
96
+ if (session.reloadLifecycle.autoMode !== current || session.autoMode !== current.reader) {
97
+ reject(new Error("JEV settings owner changed"));
98
+ return;
99
+ }
100
+ if (!value.status || typeof value.status.enabled !== "boolean") {
101
+ if (action !== "status") reject(new Error("JEV owner could not apply the request"));
102
+ else resolve(unavailable("JEV owner is not bound to this session"));
103
+ return;
104
+ }
105
+ resolve({
106
+ available: true,
107
+ enabled: value.status.enabled,
108
+ pending: value.pending === true,
109
+ mode: safeText(value.status.mode),
110
+ storage: value.status.storage === null ? null : safeText(value.status.storage),
111
+ remaining: Number.isSafeInteger(value.status.remaining) ? value.status.remaining : undefined,
112
+ availability: safeText(value.status.availability),
113
+ providerReadiness: safeText(value.status.providerReadiness),
114
+ ...(value.error ? { error: "JEV request declined or unavailable; inspect the owner Pi session." } : {}),
115
+ });
116
+ };
117
+ try {
118
+ pi.events.emit("skill-harness:jev-control-v1", {
119
+ version: 1,
120
+ requestId,
121
+ sessionId: current.nativeSessionId,
122
+ action,
123
+ reply,
124
+ });
125
+ } catch {
126
+ clearTimeout(timer);
127
+ reject(new Error("JEV control bridge failed"));
128
+ }
129
+ });
130
+ }
131
+ export async function dashboardSettingsSnapshot(
132
+ session: GrantsSession,
133
+ jev?: DashboardJevState,
134
+ ): Promise<DashboardSettingsSnapshot> {
135
+ const current = owner(session);
136
+ const limits = childExecutionLimits(session);
137
+ return {
138
+ editable: Boolean(current.authority),
139
+ wallSeconds: limits.wallMs / 1000,
140
+ idleSeconds: limits.idleMs / 1000,
141
+ wallSource: limits.wallSource,
142
+ idleSource: limits.idleSource,
143
+ descendants: session.capacity?.total ?? 0,
144
+ reserved: session.capacity?.reserved ?? 0,
145
+ maxDepth: session.maxDepth ?? 0,
146
+ perCall: MAX_CHILDREN_PER_CALL,
147
+ jev: jev ?? (await dashboardJev(session, "status")),
148
+ };
149
+ }
@@ -4,7 +4,7 @@ import { realpath } from "node:fs/promises";
4
4
  import { Type } from "typebox";
5
5
  import { shouldSeekApproval } from "../src/kernel/approval.ts";
6
6
  import { planDelegation } from "../src/kernel/delegate.ts";
7
- import type { OperationClaim } from "../src/kernel/dispatch-operation.ts";
7
+ import type { OperationClaim, OperationReference } from "../src/kernel/dispatch-operation.ts";
8
8
  import { validOperationId } from "../src/kernel/dispatch-operation.ts";
9
9
  import { GovernanceRefusal } from "../src/kernel/refusals.ts";
10
10
  import { assertDefinitionIdentity } from "./definition-describe.ts";
@@ -15,6 +15,79 @@ import type { ChildSpec, DelegationToolContext } from "./run-delegation.ts";
15
15
  import type { ExecutionOccurrenceIds } from "./execution-occurrence.ts";
16
16
  import type { DelegationOutcome } from "./execute-child.ts";
17
17
 
18
+ // Short-lived exact finals for explicit workflow retention. The operation registry/socket
19
+ // remains identity-only; losing this observation never changes the original child result.
20
+ // The native-owner lifecycle survives extension reloads, which create a new GrantsSession.
21
+ const FINAL_BYTES = 4 * 1024 * 1024,
22
+ FINAL_ENTRIES = 128;
23
+ type Final = Extract<NonNullable<DelegationOutcome["final"]>, { state: "complete" }>;
24
+ export interface OperationFinalStore {
25
+ lifecycle: object;
26
+ owner: string;
27
+ bytes: number;
28
+ entries: Map<string, { executionId: string; final: Final }>;
29
+ }
30
+ export function bindOperationFinals(session: GrantsSession, sessionId: string, cwd: string) {
31
+ const owner = JSON.stringify([sessionId, cwd]),
32
+ old = session.reloadLifecycle.operationFinals;
33
+ if (!old || old.lifecycle !== session.reloadLifecycle || old.owner !== owner)
34
+ session.reloadLifecycle.operationFinals = {
35
+ lifecycle: session.reloadLifecycle,
36
+ owner,
37
+ bytes: 0,
38
+ entries: new Map(),
39
+ };
40
+ }
41
+ /** Capture before admitting work; a late child cannot write a replacement session's store. */
42
+ export function operationFinalScope(session: GrantsSession): object | undefined {
43
+ return session.reloadLifecycle?.operationFinals;
44
+ }
45
+ export function operationFinal(session: GrantsSession, operation: OperationReference) {
46
+ const store = session.reloadLifecycle?.operationFinals,
47
+ item = store?.entries.get(operation.operationId);
48
+ if (!store || store.lifecycle !== session.reloadLifecycle || !item || item.executionId !== operation.executionId)
49
+ return {
50
+ state: "unavailable" as const,
51
+ reason:
52
+ "exact native final was not retained, expired from the 4 MiB session buffer, or belongs to an earlier session",
53
+ };
54
+ const observed = operation.runtime?.final,
55
+ final = item.final;
56
+ if (
57
+ operation.state !== "settled" ||
58
+ observed?.state !== "complete" ||
59
+ ["sessionId", "messageId", "leafId", "sha256"].some(
60
+ (key) => observed[key as keyof typeof observed] !== final[key as keyof Final],
61
+ )
62
+ )
63
+ return { state: "unavailable" as const, reason: "native final identity no longer matches the operation" };
64
+ return { ...final };
65
+ }
66
+ function retainOperationFinal(
67
+ session: GrantsSession,
68
+ claim: OperationClaim,
69
+ result: DelegationOutcome,
70
+ scope: object | undefined,
71
+ ) {
72
+ const store = session.reloadLifecycle?.operationFinals,
73
+ final = result.final;
74
+ if (!store || store !== scope || store.lifecycle !== session.reloadLifecycle || final?.state !== "complete") return;
75
+ const bytes = Buffer.byteLength(final.text);
76
+ if (!bytes || bytes > FINAL_BYTES || createHash("sha256").update(final.text).digest("hex") !== final.sha256) return;
77
+ const old = store.entries.get(claim.operation.operationId);
78
+ if (old) {
79
+ store.bytes -= Buffer.byteLength(old.final.text);
80
+ store.entries.delete(claim.operation.operationId);
81
+ }
82
+ while (store.bytes + bytes > FINAL_BYTES || store.entries.size >= FINAL_ENTRIES) {
83
+ const first = store.entries.entries().next().value!;
84
+ store.bytes -= Buffer.byteLength(first[1].final.text);
85
+ store.entries.delete(first[0]);
86
+ }
87
+ store.entries.set(claim.operation.operationId, { executionId: claim.operation.executionId, final: { ...final } });
88
+ store.bytes += bytes;
89
+ }
90
+
18
91
  export const operationIdShape = () =>
19
92
  Type.Optional(
20
93
  Type.String({
@@ -121,7 +194,12 @@ export function existingOperationOutcome(claim: OperationClaim, depth: number):
121
194
  operation: { ...claim.operation, reused: true },
122
195
  };
123
196
  }
124
- export async function finishDelegationOperation(claim: OperationClaim, result: DelegationOutcome) {
197
+ export async function finishDelegationOperation(
198
+ claim: OperationClaim,
199
+ result: DelegationOutcome,
200
+ session: GrantsSession,
201
+ finalScope: object | undefined,
202
+ ) {
125
203
  const cleanup = result.cleanup;
126
204
  const state =
127
205
  cleanup?.state === "settled"
@@ -161,6 +239,7 @@ export async function finishDelegationOperation(claim: OperationClaim, result: D
161
239
  }
162
240
  : {}),
163
241
  });
242
+ retainOperationFinal(session, claim, result, finalScope);
164
243
  result.operation = {
165
244
  ...claim.operation,
166
245
  state,
@@ -1,3 +1,4 @@
1
+ import { childExecutionLimits } from "./dashboard-settings.ts";
1
2
  import { realpath } from "node:fs/promises";
2
3
  import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
3
4
  import { retainDiscoveryCleanupFailure } from "./session.ts";
@@ -16,13 +17,7 @@ import { appendLedgerEvent, buildChildLifecycleEvent } from "../src/governance/l
16
17
  import { hasFinalizerError } from "../src/governance/finalization.ts";
17
18
  import { mergeChildEnv } from "../src/kernel/propagation.ts";
18
19
  import type { Capability } from "../src/kernel/resolve.ts";
19
- import {
20
- DEFAULT_KILL_GRACE_MS,
21
- ENV_CHILD_IDLE_TIMEOUT,
22
- ENV_CHILD_TIMEOUT,
23
- idleTimeoutFromEnv,
24
- timeoutFromEnv,
25
- } from "../src/kernel/run-child.ts";
20
+ import { DEFAULT_KILL_GRACE_MS, ENV_CHILD_IDLE_TIMEOUT, ENV_CHILD_TIMEOUT } from "../src/kernel/run-child.ts";
26
21
  import { activitySessionFor } from "../src/executors/activity-session.ts";
27
22
  import { processTreeActivity } from "../src/executors/process-activity.ts";
28
23
  import { resolveWorkspace } from "../src/executors/herdr-cli.ts";
@@ -147,6 +142,7 @@ export async function executePlannedChild(input: ChildExecutionInput): Promise<D
147
142
  const runtime = input.session.reloadLifecycle?.runtimeSettlement;
148
143
  let registered = false;
149
144
  try {
145
+ const limits = childExecutionLimits(input.session);
150
146
  input = { ...input, cwd: await realpath(input.preparedWorkspace?.workspace.root ?? input.cwd) };
151
147
  runtime?.begin(input.executionId);
152
148
  registered = Boolean(runtime);
@@ -157,6 +153,7 @@ export async function executePlannedChild(input: ChildExecutionInput): Promise<D
157
153
  input.onExecutorEntry?.();
158
154
  },
159
155
  runtime,
156
+ limits,
160
157
  );
161
158
  try {
162
159
  await runtime?.finish(
@@ -220,7 +217,8 @@ export async function executePlannedChild(input: ChildExecutionInput): Promise<D
220
217
  async function executePreparedChild(
221
218
  input: ChildExecutionInput,
222
219
  onExecutorEntry: () => void,
223
- runtime?: import("../src/governance/runtime-settlement.ts").RuntimeSettlement,
220
+ runtime: import("../src/governance/runtime-settlement.ts").RuntimeSettlement | undefined,
221
+ limits: ReturnType<typeof childExecutionLimits>,
224
222
  ): Promise<DelegationOutcome> {
225
223
  const { session, plan, childId, executionId, parentExecutionId, preparedWorkspace, signal, onProgress } = input;
226
224
  const ledgerPath = session.ledgerPath;
@@ -269,10 +267,10 @@ async function executePreparedChild(
269
267
  );
270
268
  activityStarted = true;
271
269
  } catch {}
272
- const configuredTimeoutMs = timeoutFromEnv(process.env[ENV_CHILD_TIMEOUT]);
270
+ const configuredTimeoutMs = limits.wallMs;
273
271
  // PR 3e: the working bound is inactivity; `deadlineAt` below is the runaway ceiling. Every child gets a pi session
274
272
  // file so the parent can see it working when its stdout is quiet (pi appends each message and tool result to it).
275
- const configuredIdleMs = idleTimeoutFromEnv(process.env[ENV_CHILD_IDLE_TIMEOUT]);
273
+ const configuredIdleMs = limits.idleMs;
276
274
  const activitySession = await activitySessionFor(plan.args, executionId);
277
275
  // Disposed on every path (review finding: it leaked on every throw), except when the operator keeps a Herdr pane,
278
276
  // where the interactive pi in that pane is still alive and still appending to this file.
@@ -378,7 +376,7 @@ async function executePreparedChild(
378
376
  workspaceId: preparedWorkspace?.workspace.workspaceId ?? null,
379
377
  });
380
378
  const sessionFlag = plan.args.indexOf("--session");
381
- if (sessionFlag >= 0) retention.observeSession({ source: "pi-session-file", value: plan.args[sessionFlag + 1] });
379
+ // Pi creates the file during execution; observe its exact final below, not a guaranteed pre-spawn miss.
382
380
  let releaseReason = "failed";
383
381
  let retainWriterLease = false;
384
382
  let terminalAttempted = false;
@@ -424,6 +422,7 @@ async function executePreparedChild(
424
422
  const output = await runCapturedExecution(
425
423
  {
426
424
  executionId,
425
+ terminalUi: session.executor.kind === "herdr",
427
426
  sessionPath: activitySession.path,
428
427
  onReadCleanup,
429
428
  onOwnership: async (identity) => {
@@ -1,3 +1,4 @@
1
+ import type { childExecutionLimits } from "./dashboard-settings.ts";
1
2
  /** Operator-facing execution settings; display never changes admission, deadlines or retention. */
2
3
  import { timeoutFromEnv, idleTimeoutFromEnv } from "../src/kernel/run-child.ts";
3
4
  import { timeoutMsFromEnv } from "../src/governance/approval-prompt.ts";
@@ -11,6 +12,7 @@ import {
11
12
  export function renderExecutionControls(
12
13
  nativeSessionRoot: string | undefined,
13
14
  env: NodeJS.ProcessEnv = process.env,
15
+ limits?: ReturnType<typeof childExecutionLimits>,
14
16
  ): string[] {
15
17
  let approval: string;
16
18
  try {
@@ -20,9 +22,12 @@ export function renderExecutionControls(
20
22
  approval = `${ENV_APPROVAL_TIMEOUT} invalid; a needed new prompt will refuse`;
21
23
  }
22
24
  const source = (key: string) => (env[key] === undefined ? "default" : `from ${key}; zero/invalid uses default`);
25
+ const wallSource = limits?.wallSource === "session" ? "session override; future children" : source(ENV_CHILD_TIMEOUT);
26
+ const idleSource =
27
+ limits?.idleSource === "session" ? "session override; future children" : source(ENV_CHILD_IDLE_TIMEOUT);
23
28
  return [
24
- ` child wall ${timeoutFromEnv(env[ENV_CHILD_TIMEOUT]) / 1000}s (${source(ENV_CHILD_TIMEOUT)})`,
25
- ` child idle ${idleTimeoutFromEnv(env[ENV_CHILD_IDLE_TIMEOUT]) / 1000}s (${source(ENV_CHILD_IDLE_TIMEOUT)})`,
29
+ ` child wall ${(limits?.wallMs ?? timeoutFromEnv(env[ENV_CHILD_TIMEOUT])) / 1000}s (${wallSource})`,
30
+ ` child idle ${(limits?.idleMs ?? idleTimeoutFromEnv(env[ENV_CHILD_IDLE_TIMEOUT])) / 1000}s (${idleSource})`,
26
31
  " process safety controls; no time/token budget is added to the model prompt",
27
32
  ` approval ${approval}`,
28
33
  ` transcripts ${
@@ -1,3 +1,4 @@
1
+ import type { childExecutionLimits } from "./dashboard-settings.ts";
1
2
  /**
2
3
  * The `/grants` command — session status, approvals, and ledger integrity.
3
4
  *
@@ -28,6 +29,7 @@ export interface GrantsCommandContext {
28
29
  governed: boolean;
29
30
  /** Host opt-in captured by this session, not a later environment reread. */
30
31
  nativeSessionRoot?: string;
32
+ executionLimits?: ReturnType<typeof childExecutionLimits>;
31
33
  autoModeStatus?: string;
32
34
  /** Explicit owner mutation; the command parser itself holds no permission authority. */
33
35
  changeAutoMode?: (enabled: boolean) => Promise<string>;
@@ -439,7 +441,7 @@ export const grantsCommand = {
439
441
  // ADR-0031, and its probe is only defensible because this line exists. Placed next to `holding` so the
440
442
  // two facts about what a spawn will be sit together.
441
443
  ` executor ${executor.disclosure}`,
442
- ...renderExecutionControls(ctx.grants.nativeSessionRoot),
444
+ ...renderExecutionControls(ctx.grants.nativeSessionRoot, process.env, ctx.grants.executionLimits),
443
445
  ` Auto ${ctx.grants.autoModeStatus ?? "OFF (default)"}`,
444
446
  ` depth ${depth} of max ${maxDepth}${maxDepth <= 0 ? " (spawning disabled)" : ""}`,
445
447
  ` ledger ${ledgerPath || "(not recording — set PI_DADDY_LEDGER)"}`,
@@ -1,3 +1,4 @@
1
+ import { registerDashboardSettings, childExecutionLimits } from "./dashboard-settings.ts";
1
2
  import { bindEcosystemVersions } from "./ecosystem-versions.ts";
2
3
  import {
3
4
  initializeSessionAutoMode,
@@ -90,6 +91,7 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
90
91
  session.currentThinking = typeof pi.getThinkingLevel === "function" ? () => pi.getThinkingLevel() : undefined;
91
92
  registerActivityTimeline(pi, session);
92
93
  const runtimeSnapshot = registerRuntimeSnapshot(pi, session);
94
+ registerDashboardSettings(pi, session);
93
95
  const dashboardPluginRoot = fileURLToPath(new URL("../herdr-plugin/", import.meta.url));
94
96
  const dashboardPaths = defaultDashboardPaths(agentDir());
95
97
  // Definitions are registered only after owner-bound session_start. Until then there is no delegation
@@ -481,6 +483,7 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
481
483
  }
482
484
  : {}),
483
485
  nativeSessionRoot: session.nativeSessionRoot,
486
+ executionLimits: childExecutionLimits(session),
484
487
  changeAutoMode: async (enabled: boolean) => {
485
488
  const { auto } = await setAutoApproval(session, enabled, "command");
486
489
  return `${auto.enabled ? "ON" : "OFF"} (${auto.source}; current session permissions)`;
@@ -5,6 +5,8 @@ import type { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
5
5
 
6
6
  /** The root baseline and latest child publication for one real Pi session owner. */
7
7
  export interface ReloadLifecycle {
8
+ operationFinals?: import("./delegation-operations.ts").OperationFinalStore;
9
+ executionSettings?: { nativeSessionId: string; wallMs?: number; idleMs?: number };
8
10
  autoMode?: import("./session-auto-mode.ts").SessionAutoLifecycle;
9
11
  runtimeSnapshotUnsubscribe?: () => void;
10
12
  runtimeSnapshotGeneration?: object;
@@ -115,6 +117,7 @@ export function bindReloadLifecycle(
115
117
  existing = {
116
118
  root: current,
117
119
  autoMode: existing.autoMode,
120
+ executionSettings: existing.executionSettings,
118
121
  activityRootId: existing.activityRootId,
119
122
  episodeId: existing.episodeId,
120
123
  capacity: existing.capacity,