@timqi/pier 0.1.14 → 0.2.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/README.md +44 -22
- package/dist/agent/config.js +2 -2
- package/dist/agent/events.js +32 -3
- package/dist/agent/packages.js +6 -10
- package/dist/agent/pi.js +111 -68
- package/dist/agent/roles.js +103 -0
- package/dist/agent/types.js +78 -0
- package/dist/boards/boards.js +42 -32
- package/dist/channels/chains.js +0 -4
- package/dist/channels/commands.js +7 -12
- package/dist/channels/config.js +27 -1
- package/dist/channels/control.js +5 -16
- package/dist/channels/conversations.js +0 -5
- package/dist/channels/lark-api.js +20 -26
- package/dist/channels/lark-outbound.js +16 -29
- package/dist/channels/lark-panel.js +1 -1
- package/dist/channels/lark.js +61 -39
- package/dist/channels/lines.js +33 -1
- package/dist/channels/panel.js +17 -107
- package/dist/channels/routes.js +11 -21
- package/dist/channels/runtime.js +34 -25
- package/dist/channels/slack-outbound.js +6 -15
- package/dist/channels/slack-panel.js +1 -1
- package/dist/channels/slack-render.js +1 -1
- package/dist/channels/slack.js +63 -57
- package/dist/channels/types.js +1 -1
- package/dist/config-sync.js +42 -37
- package/dist/core/chain.js +202 -0
- package/dist/core/identity.js +34 -11
- package/dist/core/inbound-file.js +4 -3
- package/dist/core/reply.js +96 -80
- package/dist/core/router.js +111 -118
- package/dist/core/types.js +36 -77
- package/dist/db.js +33 -2
- package/dist/main.js +46 -37
- package/dist/settings.js +25 -18
- package/dist/tasks/agent.js +11 -5
- package/dist/tasks/callbacks.js +55 -5
- package/dist/tasks/cli.js +22 -11
- package/dist/tasks/definitions.js +30 -17
- package/dist/tasks/execution.js +2 -2
- package/dist/tasks/groups.js +5 -2
- package/dist/tasks/messages.js +7 -7
- package/dist/tasks/open-items.js +70 -0
- package/dist/tasks/operations.js +76 -39
- package/dist/tasks/outbox.js +19 -6
- package/dist/tasks/routes.js +19 -249
- package/dist/tasks/service.js +145 -34
- package/dist/tasks/store.js +119 -56
- package/dist/tasks/types.js +5 -0
- package/dist/tools.js +2 -2
- package/dist/web/auth.js +2 -2
- package/dist/web/config-sync.js +4 -5
- package/dist/web/config.js +9 -2
- package/dist/web/explorer.js +2 -2
- package/dist/web/fs.js +16 -1
- package/dist/web/instance.js +8 -5
- package/dist/web/packages.js +1 -1
- package/dist/web/passkeys.js +3 -3
- package/dist/web/providers.js +3 -3
- package/dist/web/public/assets/code-BZwvPani.js +2 -0
- package/dist/web/public/assets/code-BZwvPani.js.br +0 -0
- package/dist/web/public/assets/code-BZwvPani.js.gz +0 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js +5 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js.br +0 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js.gz +0 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js +72 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js.br +0 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js.gz +0 -0
- package/dist/web/public/assets/{hljs-tWqyD59G.js → hljs-BRz6a8Dq.js} +2 -2
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.br +0 -0
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.gz +0 -0
- package/dist/web/public/assets/index-BpFzCwlo.js +7 -0
- package/dist/web/public/assets/index-BpFzCwlo.js.br +0 -0
- package/dist/web/public/assets/index-BpFzCwlo.js.gz +0 -0
- package/dist/web/public/assets/index-wAGxFFPj.css +2 -0
- package/dist/web/public/assets/index-wAGxFFPj.css.br +0 -0
- package/dist/web/public/assets/index-wAGxFFPj.css.gz +0 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js +8 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js.br +0 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js.gz +0 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js +9 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js.br +0 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js.gz +0 -0
- package/dist/web/public/index.html +40 -109
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +0 -2
- package/dist/web/push.js +1 -1
- package/dist/web/server.js +91 -116
- package/dist/web/session-state.js +5 -40
- package/docs/deploy.md +5 -3
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +14 -4
- package/skills/pier-help/SKILL.md +57 -23
- package/skills/pier-tasks/SKILL.md +58 -18
- package/dist/channels/handoff.js +0 -94
- package/dist/web/public/assets/activity-Ds6fCHnb.js +0 -5
- package/dist/web/public/assets/activity-Ds6fCHnb.js.br +0 -0
- package/dist/web/public/assets/activity-Ds6fCHnb.js.gz +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js +0 -1
- package/dist/web/public/assets/boards-BKCj6EwK.js.br +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js +0 -4
- package/dist/web/public/assets/explorer-DnTm975c.js.br +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js.gz +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js +0 -85
- package/dist/web/public/assets/index-BQo-haPN.js.br +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js.gz +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css +0 -2
- package/dist/web/public/assets/index-DiHj0w1i.css.br +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css.gz +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js +0 -1
- package/dist/web/public/assets/runs-C_AthWcW.js.br +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js.gz +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js +0 -5
- package/dist/web/public/assets/settings-DDaAtFlc.js.br +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js.gz +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js +0 -3
- package/dist/web/public/assets/task-runs-0pvdITiV.js.br +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js.gz +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js +0 -4
- package/dist/web/public/assets/tasks-Bz29caHJ.js.br +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js.gz +0 -0
package/dist/tasks/service.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// drain needs. Decisions belong to the files beside it.
|
|
4
4
|
import { logger } from "../log.js";
|
|
5
5
|
import { AgentTaskRunner } from "./agent.js";
|
|
6
|
-
import { TaskCallbacks } from "./callbacks.js";
|
|
6
|
+
import { DESIGN_FINAL, MILESTONE, runCwd, settleCallback, TaskCallbacks } from "./callbacks.js";
|
|
7
7
|
import { TaskDefinitions, requiredString } from "./definitions.js";
|
|
8
8
|
import { TaskExecution } from "./execution.js";
|
|
9
9
|
import { TaskGroups } from "./groups.js";
|
|
10
10
|
import { TaskMessenger } from "./messages.js";
|
|
11
|
+
import { openItems, recordOpenItems } from "./open-items.js";
|
|
11
12
|
import { TaskRunQueue } from "./runs.js";
|
|
12
13
|
import { handleTask } from "./operations.js";
|
|
13
14
|
import { isTerminal } from "./types.js";
|
|
@@ -36,17 +37,23 @@ export class TaskService {
|
|
|
36
37
|
runs;
|
|
37
38
|
execution;
|
|
38
39
|
constructor(store, factory, router, hub,
|
|
39
|
-
/** Structural: tasks/ must not import settings.ts.
|
|
40
|
+
/** Structural: tasks/ must not import settings.ts. */
|
|
40
41
|
instance) {
|
|
41
42
|
this.store = store;
|
|
42
43
|
this.factory = factory;
|
|
43
44
|
this.router = router;
|
|
44
45
|
this.hub = hub;
|
|
45
46
|
this.instance = instance;
|
|
47
|
+
const headOf = (id) => instance.continuous.chainOf(id)?.[0] ?? id;
|
|
48
|
+
const conversation = () => instance.continuous.members()[0]?.sessionId ?? null;
|
|
46
49
|
const unreachable = (sessionId, what, why) => this.unreachable(sessionId, what, why);
|
|
47
|
-
this.messages = new TaskMessenger(store, router, hub, unreachable)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
this.messages = new TaskMessenger(store, router, hub, unreachable, (runId) => {
|
|
51
|
+
const run = store.getRun(runId);
|
|
52
|
+
if (run)
|
|
53
|
+
this.status(run);
|
|
54
|
+
});
|
|
55
|
+
this.definitions = new TaskDefinitions(store, factory, router, hub, instance.systemActions);
|
|
56
|
+
this.callbacks = new TaskCallbacks(store, router, (run) => this.changed(run), unreachable, headOf, this.milestone, conversation);
|
|
50
57
|
this.groups = new TaskGroups(store, router, {
|
|
51
58
|
getRun: (id) => this.getRun(id),
|
|
52
59
|
cancel: (id) => { this.cancel(id); },
|
|
@@ -58,7 +65,7 @@ export class TaskService {
|
|
|
58
65
|
groupId,
|
|
59
66
|
}),
|
|
60
67
|
startMember: (run) => this.runs.start(run),
|
|
61
|
-
}, (group) => this.hub.emitWorkspace({ type: "task-group-changed", groupId: group.id }), unreachable);
|
|
68
|
+
}, (group) => this.hub.emitWorkspace({ type: "task-group-changed", groupId: group.id }), unreachable, headOf, this.milestone);
|
|
62
69
|
const agent = new AgentTaskRunner(factory, router, store, this.messages, (run) => this.changed(run));
|
|
63
70
|
this.execution = new TaskExecution(store, this.definitions, this.callbacks, agent, {
|
|
64
71
|
runChild: (taskId, parent) => this.run(taskId, parent.input, "task", parent.id, {
|
|
@@ -73,6 +80,64 @@ export class TaskService {
|
|
|
73
80
|
changed: (run) => this.changed(run),
|
|
74
81
|
});
|
|
75
82
|
this.runs = new TaskRunQueue(store, this.callbacks, (run) => this.execution.start(run), (run) => this.changed(run));
|
|
83
|
+
router.onTurnEnd((sessionId, text) => {
|
|
84
|
+
// Only the head's turns write the list.
|
|
85
|
+
if (instance.continuous.members()[0]?.sessionId === sessionId) {
|
|
86
|
+
try {
|
|
87
|
+
if (recordOpenItems(store, text, Date.now()))
|
|
88
|
+
hub.emitWorkspace({ type: "open-items-changed" });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
log.warn(`open items: the markers in ${sessionId}'s reply could not be written`, err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!DESIGN_FINAL.test(text))
|
|
95
|
+
return;
|
|
96
|
+
// On Pi's dispatch stack, which must not unwind.
|
|
97
|
+
try {
|
|
98
|
+
this.designFinal(sessionId, text);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
log.error(`lead ${sessionId}: a Design final: outside any run could not be recorded; main was not told`, err);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** A design lead finalized in its own session, a turn outside any run (the
|
|
106
|
+
* user confirmed there): recorded as a finished run of the lead whose result
|
|
107
|
+
* is that reply, so it reports to the lead's supervisor, and the session list and
|
|
108
|
+
* `/status` see it, exactly as a run's `Design final:` does. */
|
|
109
|
+
designFinal(sessionId, text) {
|
|
110
|
+
// A run's own turn reports through the run.
|
|
111
|
+
if (this.store.leadPhaseOf(sessionId) !== "design" || this.store.findActiveRunForTarget(sessionId)?.state === "running")
|
|
112
|
+
return;
|
|
113
|
+
const last = this.store.latestRunForTarget(sessionId);
|
|
114
|
+
if (!last)
|
|
115
|
+
return;
|
|
116
|
+
const run = this.store.transact(() => {
|
|
117
|
+
const run = this.runs.prepare(last.context.definition, null, "agent", null, {
|
|
118
|
+
invokedBySessionId: last.invokedBySessionId,
|
|
119
|
+
sourceSessionId: last.invokedBySessionId,
|
|
120
|
+
targetSessionId: sessionId,
|
|
121
|
+
sessionMode: "reuse",
|
|
122
|
+
resumedFromRunId: last.id,
|
|
123
|
+
callbackSessionId: last.callbackSessionId,
|
|
124
|
+
callbackMode: last.callbackMode,
|
|
125
|
+
background: last.background,
|
|
126
|
+
});
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
run.state = "succeeded";
|
|
129
|
+
run.startedAt = run.finishedAt = now;
|
|
130
|
+
run.result = { type: "agent", text, sessionId };
|
|
131
|
+
run.context.sessionId = sessionId;
|
|
132
|
+
settleCallback(run, this.store);
|
|
133
|
+
this.store.saveRun(run);
|
|
134
|
+
return run;
|
|
135
|
+
});
|
|
136
|
+
if (run.callbackSessionId === null)
|
|
137
|
+
log.warn(`lead ${sessionId}: Design final: recorded as run ${run.id}; the lead reports to no one`);
|
|
138
|
+
else
|
|
139
|
+
log.info(`lead ${sessionId}: Design final: outside any run, recorded as run ${run.id} for ${run.callbackSessionId}`);
|
|
140
|
+
this.runs.start(run);
|
|
76
141
|
}
|
|
77
142
|
start(tickMs = 1000) {
|
|
78
143
|
if (this.timer)
|
|
@@ -145,7 +210,7 @@ export class TaskService {
|
|
|
145
210
|
create(raw, creator = "http") {
|
|
146
211
|
return this.definitions.create(raw, creator);
|
|
147
212
|
}
|
|
148
|
-
/** `by` is how owning code says so;
|
|
213
|
+
/** `by` is how owning code says so; `pier task` has none (definitions.ts). */
|
|
149
214
|
update(id, raw, by) {
|
|
150
215
|
return this.definitions.update(id, raw, by);
|
|
151
216
|
}
|
|
@@ -155,9 +220,9 @@ export class TaskService {
|
|
|
155
220
|
archive(id, by) {
|
|
156
221
|
return this.definitions.archive(id, by);
|
|
157
222
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return this.store.listRuns(taskId,
|
|
223
|
+
/** `pier task list`'s last run: the newest `limit` of a definition's runs. */
|
|
224
|
+
listRuns(taskId, limit = 1) {
|
|
225
|
+
return this.store.listRuns(taskId, limit);
|
|
161
226
|
}
|
|
162
227
|
getRun(id) {
|
|
163
228
|
const run = this.store.getRun(id);
|
|
@@ -165,24 +230,6 @@ export class TaskService {
|
|
|
165
230
|
throw new Error(`unknown task run: ${id}`);
|
|
166
231
|
return run;
|
|
167
232
|
}
|
|
168
|
-
getRunView(id) {
|
|
169
|
-
const run = this.getRun(id);
|
|
170
|
-
return { ...run,
|
|
171
|
-
groupCallbackState: run.groupId ? this.store.getGroup(run.groupId)?.callbackState ?? null : null };
|
|
172
|
-
}
|
|
173
|
-
listMessages(runId) {
|
|
174
|
-
this.getRun(runId);
|
|
175
|
-
return this.messages.list(runId);
|
|
176
|
-
}
|
|
177
|
-
queryRuns(query = {}) {
|
|
178
|
-
return this.store.queryRuns(query);
|
|
179
|
-
}
|
|
180
|
-
activityRuns(since) {
|
|
181
|
-
return this.store.activityRuns(since);
|
|
182
|
-
}
|
|
183
|
-
recentMessages(since) {
|
|
184
|
-
return this.messages.recent(since);
|
|
185
|
-
}
|
|
186
233
|
/** Every run, not the last hour's: the card is a message in the transcript. */
|
|
187
234
|
backgroundRuns(sessionId) {
|
|
188
235
|
return this.store.listRunsForSession(sessionId, 200)
|
|
@@ -190,6 +237,28 @@ export class TaskService {
|
|
|
190
237
|
.reverse()
|
|
191
238
|
.map((run) => this.backgroundRun(run));
|
|
192
239
|
}
|
|
240
|
+
/** What the session's queue shows beside Pi's own: follow-ups waiting for it to idle. */
|
|
241
|
+
parkedMessages(sessionId) {
|
|
242
|
+
return this.store.pendingFollowUpsTo(sessionId).map((message) => ({
|
|
243
|
+
messageId: message.id,
|
|
244
|
+
runName: this.store.getRun(message.runId)?.context.definition.name ?? message.runId,
|
|
245
|
+
text: message.content,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
/** The run ledger: runs any of `sessionIds` launched, in flight or finished since `since`, at most 200. */
|
|
249
|
+
ledger(sessionIds, since) {
|
|
250
|
+
return this.store.ledgerRuns(sessionIds, since).map(ledgerRun);
|
|
251
|
+
}
|
|
252
|
+
/** Design leads no run of which has reported `Design final:`, by their creating run: the user's to finalize. */
|
|
253
|
+
openDesigns() {
|
|
254
|
+
return [...this.store.leads().values()].flatMap((lead) => {
|
|
255
|
+
const run = lead.designOpen ? this.store.getRun(lead.runId) : undefined;
|
|
256
|
+
return run ? [ledgerRun(run)] : [];
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
openItems(now = Date.now()) {
|
|
260
|
+
return openItems(this, this.router, this.instance.continuous.members().map((m) => m.sessionId), this.openDesigns(), now);
|
|
261
|
+
}
|
|
193
262
|
activeBackgroundRunCounts() {
|
|
194
263
|
return this.store.countActiveBackgroundRunsBySession();
|
|
195
264
|
}
|
|
@@ -262,15 +331,19 @@ export class TaskService {
|
|
|
262
331
|
return this.messages.control(run, fromSessionId, mode, message);
|
|
263
332
|
}
|
|
264
333
|
resume(id, message, provenance = {}) {
|
|
334
|
+
const run = this.prepareResume(this.getRun(id), message, provenance);
|
|
335
|
+
this.runs.start(run);
|
|
336
|
+
return run;
|
|
337
|
+
}
|
|
338
|
+
prepareResume(prior, message, provenance) {
|
|
265
339
|
this.refusePaused();
|
|
266
|
-
const prior = this.getRun(id);
|
|
267
340
|
if (!isTerminal(prior.state))
|
|
268
341
|
throw new Error("run must be terminal before resume");
|
|
269
342
|
if (prior.context.definition.action.type !== "agent" || !prior.targetSessionId) {
|
|
270
343
|
throw new Error("only persisted Agent runs can be resumed");
|
|
271
344
|
}
|
|
272
345
|
const prompt = requiredString(message, "message");
|
|
273
|
-
|
|
346
|
+
return this.runs.prepare(prior.context.definition, null, "agent", null, {
|
|
274
347
|
...provenance,
|
|
275
348
|
sourceSessionId: provenance.invokedBySessionId ?? prior.invokedBySessionId,
|
|
276
349
|
targetSessionId: prior.targetSessionId,
|
|
@@ -278,16 +351,46 @@ export class TaskService {
|
|
|
278
351
|
resumedFromRunId: prior.id,
|
|
279
352
|
resumePrompt: prompt,
|
|
280
353
|
});
|
|
281
|
-
this.runs.start(run);
|
|
282
|
-
return run;
|
|
283
354
|
}
|
|
355
|
+
/** A lead reports once per wave (docs/design/10-continuous-session.md
|
|
356
|
+
* §Feature lead): the result that leaves nothing owed to it in flight
|
|
357
|
+
* resumes its last run, whose own callback reaches its supervisor. */
|
|
358
|
+
milestone = (sessionId, text, settle) => {
|
|
359
|
+
if (this.store.roleOf(sessionId) !== "lead" || this.store.countOwedTo(sessionId) > 0)
|
|
360
|
+
return "plain";
|
|
361
|
+
const last = this.store.latestRunForTarget(sessionId);
|
|
362
|
+
if (!last || last.callbackSessionId === null)
|
|
363
|
+
return "plain";
|
|
364
|
+
// A running turn reports upstream when it ends; a drain leaves the result to the next boot.
|
|
365
|
+
if (!isTerminal(last.state) || this.paused)
|
|
366
|
+
return "wait";
|
|
367
|
+
try {
|
|
368
|
+
// One transaction: a crash between the resume and the marks would resume twice.
|
|
369
|
+
const run = this.store.transact(() => {
|
|
370
|
+
const run = this.prepareResume(last, `${MILESTONE}\n\n${text()}`, {
|
|
371
|
+
invokedBySessionId: last.invokedBySessionId,
|
|
372
|
+
callbackSessionId: last.callbackSessionId,
|
|
373
|
+
callbackMode: last.callbackMode,
|
|
374
|
+
background: last.background,
|
|
375
|
+
});
|
|
376
|
+
settle();
|
|
377
|
+
return run;
|
|
378
|
+
});
|
|
379
|
+
this.runs.start(run);
|
|
380
|
+
return "resumed";
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
log.error(`lead ${sessionId}: the wave's last result could not resume run ${last.id}; delivering it as a plain callback`, err);
|
|
384
|
+
return "plain";
|
|
385
|
+
}
|
|
386
|
+
};
|
|
284
387
|
/** What `pier task` asks, under the calling session's identity. */
|
|
285
388
|
handle(raw, callerSessionId) {
|
|
286
|
-
return handleTask(this, this.definitions, this.store, raw, callerSessionId);
|
|
389
|
+
return handleTask(this, this.definitions, this.store, raw, callerSessionId, this.instance.continuous);
|
|
287
390
|
}
|
|
288
391
|
/** An agent picks from names that exist right now, never from memory. */
|
|
289
392
|
async models() {
|
|
290
|
-
const menu = this.instance
|
|
393
|
+
const menu = this.instance.modelMenu();
|
|
291
394
|
if (menu.length)
|
|
292
395
|
return { source: "menu", models: menu };
|
|
293
396
|
return { source: "catalog", models: await this.factory.availableModels() };
|
|
@@ -355,12 +458,20 @@ export class TaskService {
|
|
|
355
458
|
queuedAt: run.queuedAt,
|
|
356
459
|
startedAt: run.startedAt,
|
|
357
460
|
finishedAt: run.finishedAt,
|
|
461
|
+
// A finished run's pending messages only wait for the sweep to expire them.
|
|
462
|
+
queuedMessages: isTerminal(run.state) ? 0 : this.store.countPendingFollowUps(run.id),
|
|
358
463
|
};
|
|
359
464
|
}
|
|
360
465
|
changed(run) {
|
|
361
466
|
this.hub.emitWorkspace({ type: "task-run-changed", taskId: run.taskId, runId: run.id });
|
|
467
|
+
this.status(run);
|
|
468
|
+
}
|
|
469
|
+
status(run) {
|
|
362
470
|
if (run.background && run.invokedBySessionId) {
|
|
363
471
|
this.hub.emit(run.invokedBySessionId, { type: "task-status", run: this.backgroundRun(run) });
|
|
364
472
|
}
|
|
365
473
|
}
|
|
366
474
|
}
|
|
475
|
+
function ledgerRun(run) {
|
|
476
|
+
return { runId: run.id, name: run.context.definition.name, state: run.state, targetSessionId: run.targetSessionId, cwd: runCwd(run), queuedAt: run.queuedAt, finishedAt: run.finishedAt };
|
|
477
|
+
}
|
package/dist/tasks/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Every query this area makes against pier.db, and nothing else; the schema is
|
|
2
2
|
// db.ts's migration list.
|
|
3
3
|
import { pierDb, statements, transact } from "../db.js";
|
|
4
|
+
import { createdRole } from "./types.js";
|
|
4
5
|
const clamp = (limit, cap) => Math.min(Math.max(limit, 1), cap);
|
|
5
6
|
/** Kept unmatched probes per watch: one page of history, which is all any
|
|
6
7
|
* surface lists of a probe that found nothing. */
|
|
@@ -42,11 +43,8 @@ export class TaskStore {
|
|
|
42
43
|
listDueTasks(now) {
|
|
43
44
|
return this.#many("SELECT json FROM tasks WHERE next_run_at IS NOT NULL AND next_run_at <= ?", now);
|
|
44
45
|
}
|
|
45
|
-
listRuns(taskId, limit = 50
|
|
46
|
-
return this.#many(
|
|
47
|
-
SELECT json FROM task_runs WHERE task_id = ?
|
|
48
|
-
ORDER BY queued_at DESC LIMIT ? OFFSET ?
|
|
49
|
-
`, taskId, limit, offset);
|
|
46
|
+
listRuns(taskId, limit = 50) {
|
|
47
|
+
return this.#many("SELECT json FROM task_runs WHERE task_id = ? ORDER BY queued_at DESC LIMIT ?", taskId, limit);
|
|
50
48
|
}
|
|
51
49
|
getRun(id) {
|
|
52
50
|
return this.#one("SELECT json FROM task_runs WHERE id = ?", id);
|
|
@@ -79,57 +77,15 @@ export class TaskStore {
|
|
|
79
77
|
)
|
|
80
78
|
`).run(taskId, taskId, KEPT_PROBES);
|
|
81
79
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const where = [];
|
|
86
|
-
const params = [];
|
|
87
|
-
const add = (sql, value) => {
|
|
88
|
-
if (value !== undefined) {
|
|
89
|
-
where.push(sql);
|
|
90
|
-
params.push(value);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
add("r.state = ?", query.state);
|
|
94
|
-
add("r.task_id = ?", query.taskId);
|
|
95
|
-
add("json_extract(r.json, '$.triggerSource') = ?", query.source);
|
|
96
|
-
add("r.queued_at >= ?", query.since);
|
|
97
|
-
add("r.queued_at <= ?", query.until);
|
|
98
|
-
// Keep this predicate identical to migration 17's partial index.
|
|
99
|
-
if (!query.showUnmatched)
|
|
100
|
-
where.push("NOT (r.state = 'succeeded' AND json_extract(r.json, '$.matched') IS 0)");
|
|
101
|
-
if (query.cursor) {
|
|
102
|
-
where.push("(r.queued_at, r.id) < (?, ?)");
|
|
103
|
-
params.push(query.cursor.queuedAt, query.cursor.id);
|
|
104
|
-
}
|
|
105
|
-
const limit = clamp(query.limit ?? 50, 200);
|
|
106
|
-
const rows = this.db.prepare(`
|
|
107
|
-
SELECT r.json, g.callback_state AS group_callback_state
|
|
108
|
-
FROM task_runs r LEFT JOIN task_groups g ON g.id = json_extract(r.json, '$.groupId')
|
|
109
|
-
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
110
|
-
ORDER BY r.queued_at DESC, r.id DESC LIMIT ?
|
|
111
|
-
`).all(...params, limit + 1);
|
|
112
|
-
const runs = rows.slice(0, limit).map((row) => ({
|
|
113
|
-
...JSON.parse(row.json), groupCallbackState: row.group_callback_state,
|
|
114
|
-
}));
|
|
115
|
-
const last = runs.at(-1);
|
|
116
|
-
return {
|
|
117
|
-
runs,
|
|
118
|
-
nextCursor: rows.length > limit && last ? { queuedAt: last.queuedAt, id: last.id } : null,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
/** Activity never limits live work; only its optional history is bounded. */
|
|
122
|
-
activityRuns(since) {
|
|
123
|
-
const active = this.#many("SELECT json FROM task_runs WHERE state IN ('queued', 'running') ORDER BY queued_at DESC, id DESC");
|
|
124
|
-
if (since === undefined)
|
|
125
|
-
return active;
|
|
126
|
-
const recent = this.#many(`
|
|
80
|
+
/** Runs launched by any of `sessionIds`: in flight, or finished at or after `since`. */
|
|
81
|
+
ledgerRuns(sessionIds, since) {
|
|
82
|
+
return this.#many(`
|
|
127
83
|
SELECT json FROM task_runs
|
|
128
|
-
WHERE
|
|
84
|
+
WHERE json_extract(json, '$.invokedBySessionId') IN (SELECT value FROM json_each(?))
|
|
85
|
+
AND (state IN ('queued', 'running') OR json_extract(json, '$.finishedAt') >= ?)
|
|
129
86
|
AND NOT (state = 'succeeded' AND json_extract(json, '$.matched') IS 0)
|
|
130
87
|
ORDER BY queued_at DESC, id DESC LIMIT 200
|
|
131
|
-
`, since);
|
|
132
|
-
return [...active, ...recent].sort((a, b) => b.queuedAt - a.queuedAt || b.id.localeCompare(a.id));
|
|
88
|
+
`, JSON.stringify(sessionIds), since);
|
|
133
89
|
}
|
|
134
90
|
/** A `task` action's child runs; a cancel walks them. */
|
|
135
91
|
listChildRuns(parentRunId) {
|
|
@@ -177,15 +133,96 @@ export class TaskStore {
|
|
|
177
133
|
return new Map(rows.map((row) => [row.session_id, row.n]));
|
|
178
134
|
}
|
|
179
135
|
/** `fresh` is the one mode that makes a session rather than borrowing one;
|
|
180
|
-
* these are the agent's conversations with itself, which the
|
|
136
|
+
* these are the agent's conversations with itself, which the session list does not
|
|
137
|
+
* list. A feature lead's is the user's too, so it is not one of them. */
|
|
181
138
|
taskOwnedSessionIds() {
|
|
182
139
|
const rows = this.sql(`
|
|
183
140
|
SELECT DISTINCT json_extract(json, '$.context.sessionId') AS id
|
|
184
141
|
FROM task_runs
|
|
185
142
|
WHERE json_extract(json, '$.sessionMode') = 'fresh' AND id IS NOT NULL
|
|
143
|
+
AND json_extract(json, '$.context.definition.action.launch.role') IS NOT 'lead'
|
|
186
144
|
`).all();
|
|
187
145
|
return new Set(rows.map((row) => row.id));
|
|
188
146
|
}
|
|
147
|
+
/** The fresh run that made the session; a resume or a `--session`
|
|
148
|
+
* continuation never does, so the session's role is this run's for its life. */
|
|
149
|
+
creatorOf(sessionId) {
|
|
150
|
+
return this.#one(`
|
|
151
|
+
SELECT json FROM task_runs
|
|
152
|
+
WHERE json_extract(json, '$.targetSessionId') = ? AND json_extract(json, '$.sessionMode') = 'fresh'
|
|
153
|
+
ORDER BY queued_at LIMIT 1
|
|
154
|
+
`, sessionId);
|
|
155
|
+
}
|
|
156
|
+
roleOf(sessionId) {
|
|
157
|
+
const run = this.creatorOf(sessionId);
|
|
158
|
+
return run && createdRole(run);
|
|
159
|
+
}
|
|
160
|
+
leadPhaseOf(sessionId) {
|
|
161
|
+
const run = this.creatorOf(sessionId);
|
|
162
|
+
if (!run || createdRole(run) !== "lead" || run.context.definition.action.type !== "agent")
|
|
163
|
+
return undefined;
|
|
164
|
+
return run.context.definition.action.launch?.design ? "design" : "build";
|
|
165
|
+
}
|
|
166
|
+
/** Every lead session with its phase (as `leadPhaseOf`), its creating run,
|
|
167
|
+
* whether a run targeting it is queued or running, and — a design lead's —
|
|
168
|
+
* whether no run of it has reported `Design final:` yet, which leaves the
|
|
169
|
+
* design on the user; in one statement for the session list's listing. */
|
|
170
|
+
leads() {
|
|
171
|
+
const rows = this.sql(`
|
|
172
|
+
SELECT c.id, c.run_id, c.design, l.id IS NOT NULL AS live, f.id IS NOT NULL AS final FROM (
|
|
173
|
+
SELECT id AS run_id, json_extract(json, '$.targetSessionId') AS id,
|
|
174
|
+
json_extract(json, '$.context.definition.action.launch.role') AS role,
|
|
175
|
+
json_extract(json, '$.context.definition.action.launch.design') AS design,
|
|
176
|
+
ROW_NUMBER() OVER (PARTITION BY json_extract(json, '$.targetSessionId') ORDER BY queued_at) AS n
|
|
177
|
+
FROM task_runs
|
|
178
|
+
WHERE json_extract(json, '$.sessionMode') = 'fresh' AND json_extract(json, '$.targetSessionId') IS NOT NULL
|
|
179
|
+
) c
|
|
180
|
+
LEFT JOIN (
|
|
181
|
+
SELECT DISTINCT json_extract(json, '$.targetSessionId') AS id FROM task_runs WHERE state IN ('queued', 'running')
|
|
182
|
+
) l ON l.id = c.id
|
|
183
|
+
LEFT JOIN (
|
|
184
|
+
SELECT DISTINCT json_extract(json, '$.targetSessionId') AS id FROM task_runs
|
|
185
|
+
WHERE json_extract(json, '$.result.type') = 'agent'
|
|
186
|
+
AND instr(char(10) || json_extract(json, '$.result.text'), char(10) || 'Design final:') > 0
|
|
187
|
+
) f ON f.id = c.id
|
|
188
|
+
WHERE c.n = 1 AND c.role = 'lead'
|
|
189
|
+
`).all();
|
|
190
|
+
return new Map(rows.map((r) => {
|
|
191
|
+
const phase = r.design === 1 ? "design" : "build";
|
|
192
|
+
return [r.id, { phase, runId: r.run_id, runLive: r.live === 1, designOpen: phase === "design" && r.final === 0 }];
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
/** What a milestone resumes, and whose supervisor it reports to. */
|
|
196
|
+
latestRunForTarget(sessionId) {
|
|
197
|
+
return this.#one(`
|
|
198
|
+
SELECT json FROM task_runs WHERE json_extract(json, '$.targetSessionId') = ? ORDER BY queued_at DESC, id DESC LIMIT 1
|
|
199
|
+
`, sessionId);
|
|
200
|
+
}
|
|
201
|
+
/** Runs in flight whose result is owed to the session, their own or their
|
|
202
|
+
* unfinished group's; a finished group's losers still cancelling owe nothing. */
|
|
203
|
+
countOwedTo(sessionId) {
|
|
204
|
+
const row = this.sql(`
|
|
205
|
+
SELECT COUNT(*) AS n FROM task_runs r
|
|
206
|
+
WHERE r.state IN ('queued', 'running') AND (
|
|
207
|
+
json_extract(r.json, '$.callbackSessionId') = ?
|
|
208
|
+
OR EXISTS (SELECT 1 FROM task_groups g WHERE g.id = json_extract(r.json, '$.groupId')
|
|
209
|
+
AND g.finished_at IS NULL AND json_extract(g.json, '$.callbackSessionId') = ?))
|
|
210
|
+
`).get(sessionId, sessionId);
|
|
211
|
+
return row.n;
|
|
212
|
+
}
|
|
213
|
+
/** Whether a result is still coming to the session: a run in flight owed
|
|
214
|
+
* to it, or a finished run's or group's callback not yet delivered. */
|
|
215
|
+
awaitsResults(sessionId) {
|
|
216
|
+
if (this.countOwedTo(sessionId) > 0)
|
|
217
|
+
return true;
|
|
218
|
+
const row = this.sql(`
|
|
219
|
+
SELECT EXISTS (SELECT 1 FROM task_runs WHERE callback_state IN ('pending', 'failed')
|
|
220
|
+
AND json_extract(json, '$.callbackSessionId') = ?)
|
|
221
|
+
OR EXISTS (SELECT 1 FROM task_groups WHERE callback_state IN ('pending', 'failed')
|
|
222
|
+
AND json_extract(json, '$.callbackSessionId') = ?) AS owed
|
|
223
|
+
`).get(sessionId, sessionId);
|
|
224
|
+
return row.owed === 1;
|
|
225
|
+
}
|
|
189
226
|
listRunsForSession(sessionId, limit = 50) {
|
|
190
227
|
return this.#many(`
|
|
191
228
|
SELECT json FROM task_runs
|
|
@@ -228,8 +265,21 @@ export class TaskStore {
|
|
|
228
265
|
listMessages(runId) {
|
|
229
266
|
return this.#many("SELECT json FROM task_messages WHERE run_id = ? ORDER BY created_at", runId);
|
|
230
267
|
}
|
|
231
|
-
|
|
232
|
-
|
|
268
|
+
/** Follow-ups parked for the session until it idles: its queue shows them. */
|
|
269
|
+
pendingFollowUpsTo(sessionId) {
|
|
270
|
+
return this.#many(`
|
|
271
|
+
SELECT json FROM task_messages
|
|
272
|
+
WHERE state = 'pending' AND json_extract(json, '$.kind') = 'follow_up'
|
|
273
|
+
AND json_extract(json, '$.toSessionId') = ?
|
|
274
|
+
ORDER BY created_at
|
|
275
|
+
`, sessionId);
|
|
276
|
+
}
|
|
277
|
+
countPendingFollowUps(runId) {
|
|
278
|
+
const row = this.sql(`
|
|
279
|
+
SELECT COUNT(*) AS n FROM task_messages
|
|
280
|
+
WHERE run_id = ? AND state = 'pending' AND json_extract(json, '$.kind') = 'follow_up'
|
|
281
|
+
`).get(runId);
|
|
282
|
+
return row.n;
|
|
233
283
|
}
|
|
234
284
|
/** Each beside its run, so the sweep costs one query. A message whose run is
|
|
235
285
|
* gone comes back with `run: undefined`. */
|
|
@@ -274,4 +324,17 @@ export class TaskStore {
|
|
|
274
324
|
return run;
|
|
275
325
|
});
|
|
276
326
|
}
|
|
327
|
+
/** The continuous conversation's open items, oldest first; `runIds` as main wrote them. */
|
|
328
|
+
openItems() {
|
|
329
|
+
return this.sql("SELECT problem, stage, run_ids FROM open_items ORDER BY updated_at, rowid").all()
|
|
330
|
+
.map((r) => ({ problem: r.problem, stage: r.stage, runIds: JSON.parse(r.run_ids) }));
|
|
331
|
+
}
|
|
332
|
+
/** Main's markers, in reply order, as one write; answers how many rows changed. */
|
|
333
|
+
markOpenItems(markers, now) {
|
|
334
|
+
return this.transact(() => markers.reduce((n, m) => n + Number(m.op === "open"
|
|
335
|
+
? this.sql(`INSERT INTO open_items(problem, stage, run_ids, updated_at) VALUES (?, ?, ?, ?)
|
|
336
|
+
ON CONFLICT(problem) DO UPDATE SET stage = excluded.stage, run_ids = excluded.run_ids, updated_at = excluded.updated_at`)
|
|
337
|
+
.run(m.problem, m.stage, JSON.stringify(m.runIds), now).changes
|
|
338
|
+
: this.sql("DELETE FROM open_items WHERE problem = ?").run(m.problem).changes), 0));
|
|
339
|
+
}
|
|
277
340
|
}
|
package/dist/tasks/types.js
CHANGED
|
@@ -14,3 +14,8 @@ export const isTerminal = (state) => state === "succeeded" ||
|
|
|
14
14
|
state === "cancelled" ||
|
|
15
15
|
state === "interrupted" ||
|
|
16
16
|
state === "skipped";
|
|
17
|
+
/** The role a fresh run's session is created with and keeps: a lead's, or a
|
|
18
|
+
* worker's when a session launched it; a cron or watch run's has none. */
|
|
19
|
+
export const createdRole = ({ context: { definition: { action } }, invokedBySessionId }) => action.type === "agent" && action.launch?.role === "lead" ? "lead" : invokedBySessionId !== null ? "worker" : undefined;
|
|
20
|
+
/** The `state` of a run an item names that the ledger no longer holds; its `name` is the id. */
|
|
21
|
+
export const NOT_IN_LEDGER = "not in the ledger";
|
package/dist/tools.js
CHANGED
|
@@ -22,8 +22,8 @@ export const MANAGED = [
|
|
|
22
22
|
`post_install = ["rtk", "init", "-g", "--agent", "pi", "--auto-patch"]\n` +
|
|
23
23
|
`pre_remove = ["rtk", "init", "--uninstall", "--agent", "pi", "--global", "--auto-patch"]`,
|
|
24
24
|
summary: "Compresses long bash output before it reaches the model. Its install hook " +
|
|
25
|
-
"writes its own Pi extension (extensions/rtk.ts,
|
|
26
|
-
"
|
|
25
|
+
"writes its own Pi extension (extensions/rtk.ts), and this switch is that " +
|
|
26
|
+
"extension's too — refreshed on every update.",
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
name: "rg",
|
package/dist/web/auth.js
CHANGED
|
@@ -261,7 +261,7 @@ export function noteFailure(client) {
|
|
|
261
261
|
else
|
|
262
262
|
failures.set(client, { count: 1, resetAt: Date.now() + WINDOW_MS });
|
|
263
263
|
}
|
|
264
|
-
|
|
264
|
+
const forgetFailures = (client) => void failures.delete(client);
|
|
265
265
|
/** Hosts, not schemes: TLS commonly terminates at the reverse proxy. */
|
|
266
266
|
function originMatches(origin, host) {
|
|
267
267
|
if (!origin)
|
|
@@ -341,7 +341,7 @@ export function openSession(c, store, client) {
|
|
|
341
341
|
store.revoke(previous.id);
|
|
342
342
|
setSessionCookie(c, store.open(client, c.req.header("user-agent") ?? ""));
|
|
343
343
|
}
|
|
344
|
-
|
|
344
|
+
const PASSWORD_OFF = "Password sign-in is off while a passkey is registered.";
|
|
345
345
|
export function registerAuthRoutes(app, store,
|
|
346
346
|
/** While it answers true the password is refused and the form offers only
|
|
347
347
|
* the passkey; read per request, so removing the last passkey needs no restart. */
|
package/dist/web/config-sync.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// public route is registered before auth; every management action is after it.
|
|
3
3
|
import { logger } from "../log.js";
|
|
4
4
|
const log = logger("config-sync");
|
|
5
|
+
/** The one answer every route that writes a synced field gives while the
|
|
6
|
+
* subscription is on: the source owns it, and the Console says where. */
|
|
7
|
+
export const managedRefusal = (c) => c.json({ error: "Managed by the configuration subscription — pause it under Agent → Configuration sync to edit here" }, 409);
|
|
5
8
|
export function registerConfigShareRoute(app, sync) {
|
|
6
9
|
app.get("/config-sync/:token", async (c) => {
|
|
7
10
|
c.header("cache-control", "no-store");
|
|
@@ -18,13 +21,9 @@ export function registerConfigShareRoute(app, sync) {
|
|
|
18
21
|
const published = await sync.published(token);
|
|
19
22
|
if (!published)
|
|
20
23
|
return c.json({ error: "not found" }, 404);
|
|
21
|
-
c.header("etag", published.etag);
|
|
22
24
|
c.header("x-content-type-options", "nosniff");
|
|
23
|
-
const tags = c.req.header("if-none-match")?.split(",").map((value) => value.trim().replace(/^W\//, ""));
|
|
24
|
-
if (tags?.includes(published.etag) || tags?.includes("*"))
|
|
25
|
-
return c.body(null, 304);
|
|
26
25
|
c.header("content-type", "application/json; charset=utf-8");
|
|
27
|
-
return c.body(published
|
|
26
|
+
return c.body(published);
|
|
28
27
|
}
|
|
29
28
|
catch {
|
|
30
29
|
// Never let the app's path logger print a capability URL.
|
package/dist/web/config.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// The agent files a scope is configured by, through the ConfigStore. Not via
|
|
2
2
|
// web/fs.ts: a scope is "global" or a cwd Pi already knows, never a browser path.
|
|
3
3
|
import { isThinkingLevel } from "../core/types.js";
|
|
4
|
+
import { SNAPSHOT_FILES } from "../agent/types.js";
|
|
4
5
|
import { normalizeModelRef } from "../settings.js";
|
|
6
|
+
import { managedRefusal } from "./config-sync.js";
|
|
5
7
|
import { guarded } from "./route.js";
|
|
6
|
-
export function registerConfigRoutes(app, { factory, config, onConfigWritten }) {
|
|
8
|
+
export function registerConfigRoutes(app, { factory, config, onConfigWritten, subscribed = () => false }) {
|
|
9
|
+
const managed = (scope, name) => scope.kind === "global" && subscribed() && SNAPSHOT_FILES.includes(name);
|
|
7
10
|
// Only cwds Pi already knows are accepted — never an arbitrary path.
|
|
8
11
|
const parseScope = async (raw) => {
|
|
9
12
|
if (!raw || raw === "global")
|
|
@@ -19,7 +22,7 @@ export function registerConfigRoutes(app, { factory, config, onConfigWritten })
|
|
|
19
22
|
return c.json({
|
|
20
23
|
// Where this scope's files live on disk — the UI labels "Global" with it.
|
|
21
24
|
dir: scope.kind === "global" ? config.globalDir : scope.cwd,
|
|
22
|
-
files: await config.listFiles(scope),
|
|
25
|
+
files: (await config.listFiles(scope)).map((file) => ({ ...file, managed: managed(scope, file.name) })),
|
|
23
26
|
});
|
|
24
27
|
});
|
|
25
28
|
guarded(app, "GET", "/api/config/files/:name", 400, async (c) => {
|
|
@@ -38,6 +41,8 @@ export function registerConfigRoutes(app, { factory, config, onConfigWritten })
|
|
|
38
41
|
return c.json({ error: "content and expected content required" }, 400);
|
|
39
42
|
}
|
|
40
43
|
const name = c.req.param("name");
|
|
44
|
+
if (managed(scope, name))
|
|
45
|
+
return managedRefusal(c);
|
|
41
46
|
await config.writeFile(scope, name, body.content, body.expected);
|
|
42
47
|
onConfigWritten?.();
|
|
43
48
|
return c.json({ ok: true, content: await config.readFile(scope, name) });
|
|
@@ -48,6 +53,8 @@ export function registerConfigRoutes(app, { factory, config, onConfigWritten })
|
|
|
48
53
|
return c.json(await config.readDefaults());
|
|
49
54
|
});
|
|
50
55
|
guarded(app, "PUT", "/api/config/defaults", 400, async (c) => {
|
|
56
|
+
if (subscribed())
|
|
57
|
+
return managedRefusal(c);
|
|
51
58
|
const defaults = parseDefaults(await c.req.json().catch(() => null));
|
|
52
59
|
if (!defaults) {
|
|
53
60
|
return c.json({ error: "defaultModel must be {provider, id} or null; defaultThinkingLevel a reasoning level or null" }, 400);
|
package/dist/web/explorer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What git knows about a project directory, for the Files
|
|
1
|
+
// What git knows about a project directory, for the Files dialog. Every route
|
|
2
2
|
// runs git and nothing else; reading files is web/fs.ts.
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
@@ -11,7 +11,7 @@ const REF_RE = /^[^-\s][^\s]*$/;
|
|
|
11
11
|
/** Output capped: a diff is display data, not an archive. */
|
|
12
12
|
const git = async (root, ...args) => (await run("git", ["-C", root, ...args], { maxBuffer: MAX_DIFF_BYTES })).stdout;
|
|
13
13
|
export function registerExplorerRoutes(app) {
|
|
14
|
-
// Not a repo → { branch: null }: the Files
|
|
14
|
+
// Not a repo → { branch: null }: the Files dialog draws no compare, no filter.
|
|
15
15
|
guarded(app, "GET", "/api/explorer/git", 404, async (c) => {
|
|
16
16
|
c.header("cache-control", "no-store");
|
|
17
17
|
const root = await scoped(c.req.query("root"));
|