@junghanacs/entwurf 0.35.1 → 0.36.1
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/AGENTS.md +2 -2
- package/CHANGELOG.md +403 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +17 -0
- package/FAQ.md +3 -3
- package/README.md +30 -16
- package/VERIFY.md +12 -10
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +69 -5
- package/docs/setup-clean-host.md +30 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +4 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +99 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +59 -14
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js +44 -0
- package/mcp/entwurf-bridge/tsconfig.build.json +5 -0
- package/package.json +21 -16
- package/pi/pi-durable/bootstrap.mjs +12 -8
- package/pi/pi-durable/carrier/runtime.js +52 -2
- package/pi/pi-durable/carrier/subagent.js +6 -1
- package/pi/pi-durable/carrier/tui.js +31 -4
- package/pi/pi-durable/carrier-relocation.mjs +2 -2
- package/pi/pi-durable/overlay/upstream-pin.json +79 -36
- package/pi-extensions/lib/codex-fresh-preflight.ts +4 -3
- package/pi-extensions/lib/native-push/codex-ws-client.ts +129 -18
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -4
- package/pi-extensions/meta-bridge-pi-durable.ts +77 -21
- package/run.sh +116 -39
- package/scripts/check-acp-prompt-lifecycle.ts +4 -2
- package/scripts/check-acp-session-reuse.ts +4 -2
- package/scripts/check-acp-stop-reason.ts +4 -2
- package/scripts/check-acp-stream-hooks.ts +4 -2
- package/scripts/check-acp-usage-accounting.ts +4 -2
- package/scripts/check-codex-app-server-launch.ts +55 -12
- package/scripts/check-codex-native-push.ts +231 -12
- package/scripts/check-gate-qualification.ts +4 -4
- package/scripts/check-herdr-supply.ts +32 -7
- package/scripts/codex-app-server-launch.sh +54 -29
- package/scripts/codex-socket-classify.ts +46 -0
- package/scripts/emit-pi-durable-carrier.ts +159 -168
- package/scripts/fixtures/herdr-supply.json +10 -9
- package/scripts/lib/acp-gate-home.ts +75 -0
- package/scripts/lib/codex-socket-listener.ts +429 -0
- package/scripts/mutants/acp-stop-reason.json +11 -0
- package/scripts/mutants/codex-app-server-launch.json +39 -12
- package/scripts/mutants/codex-native.json +67 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/herdr-supply.json +6 -6
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +165 -16
- package/scripts/mutants/pi-floor.json +5 -5
- package/scripts/pi-durable-contact-host.mjs +1 -1
- package/scripts/smoke-codex-fresh-live.ts +31 -7
- package/scripts/smoke-pi-durable-fresh-live.ts +22 -3
- package/pi/pi-durable/overlay/runtime-contacts.patch +0 -283
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
diff --git a/packages/coding-agent/src/experimental/durable/runtime.ts b/packages/coding-agent/src/experimental/durable/runtime.ts
|
|
2
|
-
index fcd6e523..7f00e27f 100644
|
|
3
|
-
--- a/packages/coding-agent/src/experimental/durable/runtime.ts
|
|
4
|
-
+++ b/packages/coding-agent/src/experimental/durable/runtime.ts
|
|
5
|
-
@@ -8,7 +8,9 @@ import {
|
|
6
|
-
type ConversationView,
|
|
7
|
-
type Cursor,
|
|
8
|
-
type EntryRecord,
|
|
9
|
-
+ type Extension,
|
|
10
|
-
Harness,
|
|
11
|
-
+ type InputSubmissionDraft,
|
|
12
|
-
type ModelRef,
|
|
13
|
-
ROOT_CONVERSATION_ID,
|
|
14
|
-
type Submission,
|
|
15
|
-
@@ -56,8 +58,10 @@ export interface DurableView {
|
|
16
|
-
readonly conversations: readonly ConversationSummary[];
|
|
17
|
-
readonly models: readonly ModelSummary[];
|
|
18
|
-
readonly notices: readonly Notice[];
|
|
19
|
-
- /** The live task graph while the task panel is open. */
|
|
20
|
-
+ /** The live task graph, observed for the session's lifetime whether or not its panel is shown. */
|
|
21
|
-
readonly tasks?: TaskGraph;
|
|
22
|
-
+ /** Whether the task panel is shown; /tasks toggles only this. */
|
|
23
|
-
+ readonly tasksPanel: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface DurableViewSource {
|
|
27
|
-
@@ -81,6 +85,10 @@ export interface DurableController {
|
|
28
|
-
export interface OpenDurableOptions {
|
|
29
|
-
readonly cwd?: string;
|
|
30
|
-
readonly continueSession?: boolean;
|
|
31
|
-
+ /** Installed after the built-in extensions and before the Harness opens, so recovered work finds their tools. */
|
|
32
|
-
+ readonly extensions?: readonly Extension[];
|
|
33
|
-
+ /** A new session's explicit model, resolved as pi's `--provider`/`--model`; unresolved, the open fails. */
|
|
34
|
-
+ readonly model?: { readonly provider: string; readonly model: string };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface OpenDurableResult {
|
|
38
|
-
@@ -88,6 +96,8 @@ export interface OpenDurableResult {
|
|
39
|
-
readonly controller: DurableController;
|
|
40
|
-
/** pi's settings, for the TUI's theme and terminal capabilities. */
|
|
41
|
-
readonly settings: SettingsManager;
|
|
42
|
-
+ /** Admits input to the root conversation, whichever one is on screen; resolves at admission, not at the answer. */
|
|
43
|
-
+ submitToRoot(draft: InputSubmissionDraft): Promise<Submission>;
|
|
44
|
-
close(): Promise<void>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@@ -122,6 +132,9 @@ function titleOf(entry: EntryRecord | undefined): { title?: string } {
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function openDurable(options: OpenDurableOptions = {}): Promise<OpenDurableResult> {
|
|
51
|
-
+ if (options.model !== undefined && options.continueSession) {
|
|
52
|
-
+ throw new Error("An explicit model applies only to a new session, not to a continued one");
|
|
53
|
-
+ }
|
|
54
|
-
const location = await selectSession(options.cwd ?? process.cwd(), options.continueSession ?? false);
|
|
55
|
-
const envs = new ExecutionEnvs(location.cwd);
|
|
56
|
-
let harness: Harness | undefined;
|
|
57
|
-
@@ -132,6 +145,7 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
58
|
-
const settings = createHarnessSettings(settingsManager);
|
|
59
|
-
const registry = createCodingRegistry(settingsManager, location.cwd);
|
|
60
|
-
registry.install(Subagent);
|
|
61
|
-
+ for (const extension of options.extensions ?? []) registry.install(extension);
|
|
62
|
-
|
|
63
|
-
const pendingReports: unknown[] = [];
|
|
64
|
-
let report: (error: unknown) => void = (error) => pendingReports.push(error);
|
|
65
|
-
@@ -146,7 +160,29 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
66
|
-
},
|
|
67
|
-
context,
|
|
68
|
-
);
|
|
69
|
-
- const initial = location.created ? await findInitialAgentModel(settingsManager, modelRuntime) : undefined;
|
|
70
|
-
+ const initial = location.created
|
|
71
|
-
+ ? await findInitialAgentModel(settingsManager, modelRuntime, options.model)
|
|
72
|
-
+ : undefined;
|
|
73
|
-
+ if (options.model !== undefined) {
|
|
74
|
-
+ // pi resolves case-insensitive, partial and unknown ids too (a custom-id fallback with only a warning);
|
|
75
|
-
+ // an explicit model must be exactly one registered catalog model, named by its canonical id.
|
|
76
|
-
+ const asked = options.model;
|
|
77
|
-
+ const ref = initial?.model;
|
|
78
|
-
+ const thinking = initial?.thinkingLevel;
|
|
79
|
-
+ const registered = ref !== undefined && modelRuntime.getModel(ref.provider, ref.modelId) !== undefined;
|
|
80
|
-
+ const named =
|
|
81
|
-
+ ref !== undefined &&
|
|
82
|
-
+ (asked.model === ref.modelId || (thinking !== undefined && asked.model === `${ref.modelId}:${thinking}`));
|
|
83
|
-
+ if (ref === undefined || ref.provider !== asked.provider || !registered || !named) {
|
|
84
|
-
+ const resolved =
|
|
85
|
-
+ ref === undefined
|
|
86
|
-
+ ? "none"
|
|
87
|
-
+ : `${ref.provider}/${ref.modelId}${thinking === undefined ? "" : `:${thinking}`}`;
|
|
88
|
-
+ throw new Error(
|
|
89
|
-
+ `explicit-model-not-exact: asked ${asked.provider}/${asked.model}, resolved ${resolved} (registered=${registered})`,
|
|
90
|
-
+ );
|
|
91
|
-
+ }
|
|
92
|
-
+ }
|
|
93
|
-
const root = await harness.root(context, {
|
|
94
|
-
agent: {
|
|
95
|
-
cwd: location.cwd,
|
|
96
|
-
@@ -179,6 +215,7 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
97
|
-
conversations: summaries,
|
|
98
|
-
models: models(),
|
|
99
|
-
notices: [],
|
|
100
|
-
+ tasksPanel: true,
|
|
101
|
-
};
|
|
102
|
-
const listeners = new Set<() => void>();
|
|
103
|
-
let notifying = false;
|
|
104
|
-
@@ -296,17 +333,8 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
105
|
-
const thinking: ModelThinkingLevel = agentOf(state.conversation).thinkingLevel ?? "off";
|
|
106
|
-
await current.configure({ model: ref, thinkingLevel: clampThinkingLevel(model, thinking) }, context);
|
|
107
|
-
}),
|
|
108
|
-
- toggleTasks: () =>
|
|
109
|
-
- command(async () => {
|
|
110
|
-
- if (tasks !== undefined) {
|
|
111
|
-
- closeTasks();
|
|
112
|
-
- update({ tasks: undefined });
|
|
113
|
-
- return;
|
|
114
|
-
- }
|
|
115
|
-
- const graph = await opened.taskGraph(context);
|
|
116
|
-
- tasks = graph;
|
|
117
|
-
- unsubscribeTasks = graph.subscribe((value) => update({ tasks: value }));
|
|
118
|
-
- }),
|
|
119
|
-
+ // Only the panel: the graph stays observed, so live background work is never out of view.
|
|
120
|
-
+ toggleTasks: () => command(async () => update({ tasksPanel: !state.tasksPanel })),
|
|
121
|
-
switchConversation: (id) =>
|
|
122
|
-
command(async () => {
|
|
123
|
-
const next = await opened.conversation(id, context);
|
|
124
|
-
@@ -326,8 +354,11 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
125
|
-
notice("warning", `Saved model is unavailable: ${saved.provider}/${saved.modelId}`);
|
|
126
|
-
}
|
|
127
|
-
if (initial?.fallbackMessage !== undefined) notice("info", initial.fallbackMessage);
|
|
128
|
-
- // The task panel starts open; /tasks hides it.
|
|
129
|
-
- await controller.toggleTasks();
|
|
130
|
-
+ // The task graph is observed until close; its panel starts open and /tasks hides it.
|
|
131
|
-
+ const graph = await opened.taskGraph(context);
|
|
132
|
-
+ tasks = graph;
|
|
133
|
-
+ update({ tasks: graph.value });
|
|
134
|
-
+ unsubscribeTasks = graph.subscribe((value) => update({ tasks: value }));
|
|
135
|
-
// Recovered work from an interrupted turn continues now.
|
|
136
|
-
harness.resume();
|
|
137
|
-
|
|
138
|
-
@@ -342,6 +373,7 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
139
|
-
},
|
|
140
|
-
controller,
|
|
141
|
-
settings: settingsManager,
|
|
142
|
-
+ submitToRoot: (draft) => root.submit(draft, context),
|
|
143
|
-
close() {
|
|
144
|
-
closing ??= (async () => {
|
|
145
|
-
unsubscribe();
|
|
146
|
-
diff --git a/packages/coding-agent/src/experimental/durable/tui.ts b/packages/coding-agent/src/experimental/durable/tui.ts
|
|
147
|
-
index 397b276c..6a8fce8f 100644
|
|
148
|
-
--- a/packages/coding-agent/src/experimental/durable/tui.ts
|
|
149
|
-
+++ b/packages/coding-agent/src/experimental/durable/tui.ts
|
|
150
|
-
@@ -23,12 +23,14 @@ import {
|
|
151
|
-
SelectList,
|
|
152
|
-
type SelectListTheme,
|
|
153
|
-
Spacer,
|
|
154
|
-
+ type StackEntry,
|
|
155
|
-
setCapabilityOverrides,
|
|
156
|
-
setKeybindings,
|
|
157
|
-
Text,
|
|
158
|
-
TruncatedText,
|
|
159
|
-
TuiAltScreen,
|
|
160
|
-
VStack,
|
|
161
|
-
+ visibleWidth,
|
|
162
|
-
} from "@earendil-works/pi-tui";
|
|
163
|
-
import { getAgentDir } from "../../config.ts";
|
|
164
|
-
import { KeybindingsManager } from "../../core/keybindings.ts";
|
|
165
|
-
@@ -150,10 +152,75 @@ interface Handlers {
|
|
166
|
-
cycleThinking(): void;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
+/**
|
|
170
|
-
+ * The live background-task badge: the dock's first row, never shrunk, so live background work stays on
|
|
171
|
-
+ * screen however small the terminal gets. It counts the graph's background tasks whatever their kind and
|
|
172
|
-
+ * names the ones that are not running; with none it renders nothing and the dock is upstream's.
|
|
173
|
-
+ */
|
|
174
|
-
+export class TaskBadge implements Component {
|
|
175
|
-
+ #count = 0;
|
|
176
|
-
+ #detail = "";
|
|
177
|
-
+
|
|
178
|
-
+ get count(): number {
|
|
179
|
-
+ return this.#count;
|
|
180
|
-
+ }
|
|
181
|
-
+
|
|
182
|
-
+ set(graph: TaskGraph | undefined): void {
|
|
183
|
-
+ const nodes = Object.values(graph?.tasks ?? {}).filter((node) => node.background);
|
|
184
|
-
+ const tally = (state: string, match: (node: TaskGraphNode) => boolean): string[] => {
|
|
185
|
-
+ const n = nodes.filter(match).length;
|
|
186
|
-
+ return n === 0 ? [] : [`${n} ${state}`];
|
|
187
|
-
+ };
|
|
188
|
-
+ this.#count = nodes.length;
|
|
189
|
-
+ this.#detail = [
|
|
190
|
-
+ ...tally("aborting", (node) => node.abortRequested),
|
|
191
|
-
+ ...tally("completing", (node) => !node.abortRequested && node.state.status === "completing"),
|
|
192
|
-
+ ...tally("waiting", (node) => !node.abortRequested && node.state.status === "waiting"),
|
|
193
|
-
+ ...tally("pending", (node) => !node.abortRequested && node.state.status === "pending"),
|
|
194
|
-
+ ].join(", ");
|
|
195
|
-
+ }
|
|
196
|
-
+
|
|
197
|
-
+ /** The widest form that fits, padded when there is room; narrower terminals drop detail, then words. */
|
|
198
|
-
+ render(width: number): string[] {
|
|
199
|
-
+ const n = this.#count;
|
|
200
|
-
+ if (n === 0) return [];
|
|
201
|
-
+ const tasks = `⏳ ${n} ${n === 1 ? "task" : "tasks"}`;
|
|
202
|
-
+ const detailed = this.#detail ? [`${tasks} (${this.#detail})`] : [];
|
|
203
|
-
+ const forms = [...detailed, tasks, `⏳ ${n}`, `⏳${n}`, `*${n}`, "*+", "*"];
|
|
204
|
-
+ for (const form of forms) {
|
|
205
|
-
+ if (visibleWidth(form) < width) return [theme.fg("accent", ` ${form}`)];
|
|
206
|
-
+ if (visibleWidth(form) === width) return [theme.fg("accent", form)];
|
|
207
|
-
+ }
|
|
208
|
-
+ return [theme.fg("accent", "*")];
|
|
209
|
-
+ }
|
|
210
|
-
+
|
|
211
|
-
+ invalidate(): void {}
|
|
212
|
-
+}
|
|
213
|
-
+
|
|
214
|
-
+/**
|
|
215
|
-
+ * The screen: the transcript above the dock. The badge is the dock's first row and never shrinks, so the
|
|
216
|
-
+ * renderer, which clips from the bottom, drops the editor and the footer before it; a one-row terminal
|
|
217
|
-
+ * with live background work shows the badge instead of the transcript.
|
|
218
|
-
+ */
|
|
219
|
-
+export function durableLayout(transcript: Component, badge: TaskBadge, dock: readonly StackEntry[]): VStack {
|
|
220
|
-
+ return new VStack([
|
|
221
|
-
+ {
|
|
222
|
-
+ component: transcript,
|
|
223
|
-
+ basis: 0,
|
|
224
|
-
+ grow: 1,
|
|
225
|
-
+ shrink: 1,
|
|
226
|
-
+ minSize: 1,
|
|
227
|
-
+ visible: (viewport) => badge.count === 0 || viewport.height > 1,
|
|
228
|
-
+ },
|
|
229
|
-
+ { component: new VStack([{ component: badge, shrink: 0 }, ...dock]), basis: "auto", grow: 0, shrink: 1, minSize: 1 },
|
|
230
|
-
+ ]);
|
|
231
|
-
+}
|
|
232
|
-
+
|
|
233
|
-
class DurableTui {
|
|
234
|
-
static readonly #renderers: Record<string, ToolRenderers> = createAllToolRenderers();
|
|
235
|
-
readonly #ui: TuiAltScreen;
|
|
236
|
-
readonly #chat = new Container();
|
|
237
|
-
+ readonly #taskBadge = new TaskBadge();
|
|
238
|
-
readonly #tasks = new Container();
|
|
239
|
-
readonly #queue = new Container();
|
|
240
|
-
readonly #notices = new Container();
|
|
241
|
-
@@ -216,15 +283,9 @@ class DurableTui {
|
|
242
|
-
content.addChild(new Spacer(1));
|
|
243
|
-
const transcript = new ScrollView(content, { follow: "end", primary: true, overscroll: "chain" });
|
|
244
|
-
this.#transcript = transcript;
|
|
245
|
-
- const dock = new VStack([
|
|
246
|
-
- { component: this.#tasks, shrink: 1, minSize: 0 },
|
|
247
|
-
- { component: this.#queue, shrink: 1, minSize: 0 },
|
|
248
|
-
- { component: this.#notices, shrink: 1, minSize: 0 },
|
|
249
|
-
- { component: this.#editorContainer, shrink: 1, minSize: 3 },
|
|
250
|
-
- { component: this.#footer, shrink: 1, minSize: 0 },
|
|
251
|
-
- ]);
|
|
252
|
-
for (const component of [
|
|
253
|
-
this.#chat,
|
|
254
|
-
+ this.#taskBadge,
|
|
255
|
-
this.#tasks,
|
|
256
|
-
this.#queue,
|
|
257
|
-
this.#notices,
|
|
258
|
-
@@ -234,9 +295,12 @@ class DurableTui {
|
|
259
|
-
this.#ui.addChild(component);
|
|
260
|
-
}
|
|
261
|
-
this.#ui.setLayoutRoot(
|
|
262
|
-
- new VStack([
|
|
263
|
-
- { component: transcript, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
|
264
|
-
- { component: dock, basis: "auto", grow: 0, shrink: 1, minSize: 1 },
|
|
265
|
-
+ durableLayout(transcript, this.#taskBadge, [
|
|
266
|
-
+ { component: this.#tasks, shrink: 1, minSize: 0 },
|
|
267
|
-
+ { component: this.#queue, shrink: 1, minSize: 0 },
|
|
268
|
-
+ { component: this.#notices, shrink: 1, minSize: 0 },
|
|
269
|
-
+ { component: this.#editorContainer, shrink: 1, minSize: 3 },
|
|
270
|
-
+ { component: this.#footer, shrink: 1, minSize: 0 },
|
|
271
|
-
]),
|
|
272
|
-
);
|
|
273
|
-
this.#ui.setFocus(this.#editor);
|
|
274
|
-
@@ -299,7 +363,8 @@ class DurableTui {
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
- this.#syncTasks(view.tasks);
|
|
279
|
-
+ this.#taskBadge.set(view.tasks);
|
|
280
|
-
+ this.#syncTasks(view.tasksPanel ? view.tasks : undefined);
|
|
281
|
-
this.#syncQueue((view.conversation.docs["pi.inbox"] ?? { items: [] }) as InboxState);
|
|
282
|
-
this.#syncNotices(view);
|
|
283
|
-
this.#editor.borderColor = theme.getThinkingBorderColor(agentOf(view.conversation).thinkingLevel ?? "off");
|