pwragent 1.3.9 → 1.3.11
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 +15 -1
- package/dist/federation-core.js +1781 -11
- package/package.json +5 -4
- package/src/cli.js +14 -2
- package/src/daemon.js +1 -1
- package/src/federation-backend.js +57 -131
- package/src/federation-core.ts +3 -0
- package/src/federation.js +3 -3
- package/src/runtime.js +5 -1
- package/src/version.js +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pwragent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Experimental headless PwrAgent daemon and command-line client.",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@pwragent/federation": "workspace:*",
|
|
55
55
|
"@pwragent/shared": "workspace:*",
|
|
56
|
-
"
|
|
56
|
+
"@pwragent/owner-core": "workspace:*",
|
|
57
|
+
"esbuild": "0.28.2",
|
|
57
58
|
"@pwragent/agent-tools": "workspace:*"
|
|
58
59
|
},
|
|
59
60
|
"dependencies": {
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"node-pty"
|
|
64
65
|
],
|
|
65
66
|
"pwragentRelease": {
|
|
66
|
-
"tag": "v1.3.
|
|
67
|
-
"commit": "
|
|
67
|
+
"tag": "v1.3.11",
|
|
68
|
+
"commit": "7b787f4a90bfff4811f1d41b04ae025f0cfe6ebf"
|
|
68
69
|
}
|
|
69
70
|
}
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
6
6
|
import { call } from "./client.js";
|
|
7
7
|
import { serve } from "./daemon.js";
|
|
8
8
|
import { ACTIVE } from "./store.js";
|
|
9
|
+
import { formatVersion, installedVersion, packageVersion, restartRequired } from "./version.js";
|
|
9
10
|
|
|
10
11
|
const help = `PwrAgent Headless (experimental, Linux/macOS, Node >=22)
|
|
11
12
|
|
|
@@ -16,6 +17,7 @@ const help = `PwrAgent Headless (experimental, Linux/macOS, Node >=22)
|
|
|
16
17
|
pwragent project-add|project-remove --path /workspace/project
|
|
17
18
|
pwragent git-auth-configure --config-file /outpost/git-auth-settings.json
|
|
18
19
|
pwragent status | list
|
|
20
|
+
pwragent --version | version
|
|
19
21
|
pwragent submit --id JOB --prompt-file /task.txt [--sandbox read-only|workspace-write]
|
|
20
22
|
[--model MODEL] [--thread THREAD_ID] [--cwd /workspace/project]
|
|
21
23
|
pwragent show|read|interrupt|wait --id JOB
|
|
@@ -36,6 +38,7 @@ No provisioning, login, queue, automatic replay, or local messaging providers.
|
|
|
36
38
|
export async function main(args) {
|
|
37
39
|
const { values, positionals } = parseArgs({ args, allowPositionals: true, strict: true, options: {
|
|
38
40
|
help: { type: "boolean", short: "h" },
|
|
41
|
+
version: { type: "boolean" },
|
|
39
42
|
profile: { type: "string" },
|
|
40
43
|
workspace: { type: "string" },
|
|
41
44
|
"codex-home": { type: "string" },
|
|
@@ -52,7 +55,9 @@ export async function main(args) {
|
|
|
52
55
|
peer: { type: "string" },
|
|
53
56
|
name: { type: "string" }, remote: { type: "string" }, path: { type: "string" }, cwd: { type: "string" }, "config-file": { type: "string" },
|
|
54
57
|
} });
|
|
55
|
-
if (values.help
|
|
58
|
+
if (values.help) { console.log(help); return 0; }
|
|
59
|
+
if (values.version) { console.log(formatVersion(packageVersion())); return 0; }
|
|
60
|
+
if (!positionals.length) { console.log(help); return 0; }
|
|
56
61
|
if (positionals.length !== 1) throw new Error("Expected one command; see --help");
|
|
57
62
|
const command = positionals[0];
|
|
58
63
|
const allowed = {
|
|
@@ -60,7 +65,7 @@ export async function main(args) {
|
|
|
60
65
|
submit: ["id", "prompt-file", "thread", "model", "sandbox", "cwd"],
|
|
61
66
|
"project-list": [], "project-create": ["name", "label"], "project-clone": ["name", "remote", "label"],
|
|
62
67
|
"project-add": ["path", "label"], "project-remove": ["path"], "git-auth-configure": ["config-file"],
|
|
63
|
-
show: ["id"], read: ["id"], interrupt: ["id"], wait: ["id"], status: [], list: [],
|
|
68
|
+
show: ["id"], read: ["id"], interrupt: ["id"], wait: ["id"], status: [], list: [], version: [],
|
|
64
69
|
"federation-join": ["invite-file", "gateway", "label"],
|
|
65
70
|
"federation-invite": ["port", "label"],
|
|
66
71
|
"federation-status": [], "federation-leave": [], "federation-revoke": ["peer"],
|
|
@@ -69,6 +74,7 @@ export async function main(args) {
|
|
|
69
74
|
for (const option of Object.keys(values)) {
|
|
70
75
|
if (option !== "profile" && !allowed[command].includes(option)) throw new Error(`--${option} does not apply to ${command}`);
|
|
71
76
|
}
|
|
77
|
+
if (command === "version") { console.log(JSON.stringify(packageVersion())); return 0; }
|
|
72
78
|
const profile = values.profile || process.env.PWRAGENT_PROFILE || "default";
|
|
73
79
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(profile)) throw new Error("Invalid profile name");
|
|
74
80
|
const root = process.env.PWRAGENT_HOME || path.join(homedir(), ".pwragent");
|
|
@@ -118,6 +124,12 @@ export async function main(args) {
|
|
|
118
124
|
await delay(1000);
|
|
119
125
|
}
|
|
120
126
|
}
|
|
127
|
+
if (command === "status") {
|
|
128
|
+
const status = await call(directory, "status", {});
|
|
129
|
+
const installed = installedVersion();
|
|
130
|
+
console.log(JSON.stringify({ ...status, installed, restartRequired: restartRequired(status.running, installed) }));
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
121
133
|
console.log(JSON.stringify(await call(directory, command, { id: values.id })));
|
|
122
134
|
return 0;
|
|
123
135
|
}
|
package/src/daemon.js
CHANGED
|
@@ -116,7 +116,7 @@ export async function serve({ directory, workspace, home, command, profile }) {
|
|
|
116
116
|
});
|
|
117
117
|
if (shutdown) { server.close(); return await done; }
|
|
118
118
|
server.on("error", (error) => { void stop(error); });
|
|
119
|
-
console.log(JSON.stringify({ ready: true, socket: socketPath, pid: process.pid }));
|
|
119
|
+
console.log(JSON.stringify({ ready: true, socket: socketPath, pid: process.pid, running: runtime.running }));
|
|
120
120
|
return await done;
|
|
121
121
|
} catch (error) {
|
|
122
122
|
await stop(error);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { FEDERATION_BACKEND_METHOD_CAPABILITIES, FEDERATION_MAX_FRAME_BYTES, fitThreadReplayWithinByteBudget, mergeCausalMessagingContext, pageThreadReplay, shortenDerivedThreadTitle } from "../dist/federation-core.js";
|
|
3
|
+
import { buildThreadIdentityKey, NavigationQueryStore, FEDERATION_BACKEND_METHOD_CAPABILITIES, FEDERATION_MAX_FRAME_BYTES, fitThreadReplayWithinByteBudget, mergeCausalMessagingContext, pageThreadReplay, shortenDerivedThreadTitle } from "../dist/federation-core.js";
|
|
5
4
|
import { ACTIVE, atomicJson, digest } from "./store.js";
|
|
6
5
|
|
|
7
6
|
// The owner methods this outpost serves. Each one's capability comes from the
|
|
@@ -23,6 +22,11 @@ export function ownerMethodCapabilities(methods, contract = FEDERATION_BACKEND_M
|
|
|
23
22
|
}
|
|
24
23
|
export const METHOD_CAPABILITIES = ownerMethodCapabilities(OWNER_METHODS);
|
|
25
24
|
|
|
25
|
+
// The placeholder title desktop gives a new thread. Viewers recognize this
|
|
26
|
+
// exact text as "no name yet".
|
|
27
|
+
export const PLACEHOLDER_THREAD_TITLE = "Untitled thread";
|
|
28
|
+
const LEGACY_PLACEHOLDER_THREAD_TITLE = "New outpost thread";
|
|
29
|
+
|
|
26
30
|
function mode(sandbox) { return sandbox === "workspace-write" ? "auto" : "default"; }
|
|
27
31
|
function sandboxFor(params, fallback = "read-only") {
|
|
28
32
|
if (params.executionMode && !["default", "auto", "full-access"].includes(params.executionMode)) throw new Error("Unsupported execution mode");
|
|
@@ -42,16 +46,24 @@ function textInput(input) {
|
|
|
42
46
|
export class HeadlessBackend {
|
|
43
47
|
constructor(runtime, publish) {
|
|
44
48
|
this.runtime = runtime;
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
49
|
+
this.navigation = new NavigationQueryStore();
|
|
50
|
+
this.publish = (event, eventClass) => {
|
|
51
|
+
const thread = this.threads.get(event.notification.params.threadId);
|
|
52
|
+
// Acknowledging an older displayed update does not mark newer work read.
|
|
53
|
+
if (event.notification.method !== "navigation/thread/seen" || (thread && !this.summary(thread).inbox.inInbox)) {
|
|
54
|
+
this.navigation.observeAttentionEvent(event);
|
|
55
|
+
}
|
|
56
|
+
publish(event, eventClass);
|
|
57
|
+
};
|
|
47
58
|
// Same headroom as desktop's FEDERATION_RESPONSE_BYTE_BUDGET.
|
|
48
59
|
this.replayByteBudget = FEDERATION_MAX_FRAME_BYTES - 64 * 1024;
|
|
49
|
-
this.attentionViews = new Map();
|
|
50
60
|
this.file = path.join(path.dirname(runtime.store.directory), "threads.json");
|
|
51
61
|
try {
|
|
52
62
|
const data = JSON.parse(readFileSync(this.file, "utf8"));
|
|
53
63
|
if (data.version !== 1 || !Array.isArray(data.threads)) throw new Error("Invalid headless thread registry");
|
|
54
|
-
this.threads = new Map(data.threads.map((thread) => [thread.id,
|
|
64
|
+
this.threads = new Map(data.threads.map((thread) => [thread.id,
|
|
65
|
+
thread.titleSource === "fallback" && thread.title === LEGACY_PLACEHOLDER_THREAD_TITLE
|
|
66
|
+
? { ...thread, title: PLACEHOLDER_THREAD_TITLE } : thread]));
|
|
55
67
|
} catch (error) {
|
|
56
68
|
if (error.code !== "ENOENT") throw error;
|
|
57
69
|
this.threads = new Map();
|
|
@@ -113,7 +125,16 @@ export class HeadlessBackend {
|
|
|
113
125
|
}
|
|
114
126
|
if (message.method === "headless/jobChanged") {
|
|
115
127
|
this.syncJobs([message.params]);
|
|
116
|
-
|
|
128
|
+
const job = message.params;
|
|
129
|
+
if (job.threadId && this.latest(job.threadId) === job) {
|
|
130
|
+
const active = ACTIVE.has(job.status);
|
|
131
|
+
// An accepted job starts before Codex assigns its turn ID. Let the
|
|
132
|
+
// shared reducer adopt that ID without allocating another start rank.
|
|
133
|
+
this.navigation.observeAttentionEvent({ backend: "codex", notification: {
|
|
134
|
+
method: active ? "turn/started" : "turn/completed",
|
|
135
|
+
params: { threadId: job.threadId, turnId: job.turnId || (active ? undefined : job.id) },
|
|
136
|
+
} });
|
|
137
|
+
}
|
|
117
138
|
this.publish({ backend: "codex", notification: { method: "navigation/invalidated", params: { threadId: message.params.threadId, sourceMethod: "turn/completed" } } }, "navigation");
|
|
118
139
|
return;
|
|
119
140
|
}
|
|
@@ -139,7 +160,11 @@ export class HeadlessBackend {
|
|
|
139
160
|
switch (name) {
|
|
140
161
|
case "listBackends": return this.listBackends();
|
|
141
162
|
case "listThreads": return { backend: "codex", fetchedAt: Date.now(), threads: [...this.threads.values()].filter((thread) => !thread.archived).map((thread) => this.summary(thread)), workspaceRoots: this.runtime.projects.list().map((project) => project.path) };
|
|
142
|
-
case "getNavigationQueryPage":
|
|
163
|
+
case "getNavigationQueryPage": {
|
|
164
|
+
if (params.inventory === "viewer") throw new Error("Expected owner navigation protocol 2");
|
|
165
|
+
return this.navigation.readPage({ request: params, scopeKey: envelope.sourceInstanceId || "local",
|
|
166
|
+
loadIndex: async () => this.navigationIndex() });
|
|
167
|
+
}
|
|
143
168
|
case "getNavigationLaunchpadConfig": {
|
|
144
169
|
if (params.protocol !== 2) throw new Error("Unsupported navigation protocol");
|
|
145
170
|
if (params.directoryKey !== undefined) this.runtime.projects.resolve(params.directoryKey);
|
|
@@ -148,7 +173,7 @@ export class HeadlessBackend {
|
|
|
148
173
|
}
|
|
149
174
|
case "getNavigationSelectedDetail": return { protocol: 2, ref: params.ref, revision: digest(this.summary(this.owned(params.ref.threadId))), readiness: "ready", identity: "present", thread: this.summary(this.owned(params.ref.threadId)), collections: [] };
|
|
150
175
|
case "getNavigationQueueProjection": return { protocol: 2, ref: params.ref, revision: "no-queue", readiness: "ready", complete: true, entries: [] };
|
|
151
|
-
case "releaseNavigationAttentionView": this.
|
|
176
|
+
case "releaseNavigationAttentionView": this.navigation.releaseAttentionView(envelope.sourceInstanceId || "local", params.viewId); return {};
|
|
152
177
|
case "resolveThread": return this.threads.has(params.threadId) ? { thread: this.summary(this.owned(params.threadId)) } : {};
|
|
153
178
|
case "resolveThreadAdmissionState": {
|
|
154
179
|
const thread = this.summary(this.owned(params.threadId));
|
|
@@ -210,7 +235,6 @@ export class HeadlessBackend {
|
|
|
210
235
|
const thread = this.owned(params.threadId);
|
|
211
236
|
const title = params.title || params.name;
|
|
212
237
|
if (typeof title !== "string" || !title.trim() || title.length > 200) throw new Error("Invalid title");
|
|
213
|
-
thread.updatedAt = Date.now();
|
|
214
238
|
this.setTitle(thread, title.trim(), "explicit");
|
|
215
239
|
return { backend: "codex", threadId: thread.id, title: thread.title };
|
|
216
240
|
}
|
|
@@ -219,13 +243,17 @@ export class HeadlessBackend {
|
|
|
219
243
|
for (const key of ["seenAt", "seenUpdatedAt"]) {
|
|
220
244
|
if (params[key] !== undefined && (!Number.isFinite(params[key]) || params[key] < 0)) throw new Error(`Invalid ${key}`);
|
|
221
245
|
}
|
|
222
|
-
|
|
246
|
+
// An omitted watermark keeps the stored one (see MarkThreadSeenRequest).
|
|
247
|
+
// A thread never given one is pinned to the seenAt it was judged by,
|
|
248
|
+
// so recording a new seenAt cannot change its unread state.
|
|
249
|
+
thread.seenUpdatedAt = params.seenUpdatedAt ?? thread.seenUpdatedAt ?? thread.seenAt ?? 0;
|
|
223
250
|
thread.seenAt = params.seenAt ?? Date.now();
|
|
224
|
-
thread.seenUpdatedAt = seenUpdatedAt;
|
|
225
251
|
this.persist();
|
|
226
|
-
this.
|
|
227
|
-
|
|
228
|
-
|
|
252
|
+
this.publish({ backend: "codex", notification: { method: "navigation/thread/seen",
|
|
253
|
+
params: { threadId: thread.id, ...(params.seenUpdatedAt !== undefined ? { seenUpdatedAt: params.seenUpdatedAt } : {}) } } }, "transcript");
|
|
254
|
+
this.invalidate(thread.id, "navigation/thread/seen");
|
|
255
|
+
return { backend: "codex", threadId: thread.id, seenAt: thread.seenAt,
|
|
256
|
+
...(params.seenUpdatedAt !== undefined ? { seenUpdatedAt: params.seenUpdatedAt } : {}) };
|
|
229
257
|
}
|
|
230
258
|
case "setThreadExecutionMode": {
|
|
231
259
|
const thread = this.owned(params.threadId);
|
|
@@ -315,11 +343,11 @@ export class HeadlessBackend {
|
|
|
315
343
|
const result = await this.runtime.codex.request("thread/start", { cwd, dynamicTools: this.runtime.messagingTools.buildDynamicToolSpecs(), approvalPolicy: "never", sandbox, ...(params.model ? { model: params.model } : {}), ephemeral: false });
|
|
316
344
|
if (!result?.thread?.id || typeof result.thread.id !== "string" || result.thread.id.length > 256) throw new Error("Invalid thread/start response");
|
|
317
345
|
this.runtime.loadedThreads.add(result.thread.id);
|
|
318
|
-
const thread = { id: result.thread.id, messagingFileTools: true, title:
|
|
346
|
+
const thread = { id: result.thread.id, messagingFileTools: true, title: PLACEHOLDER_THREAD_TITLE, titleSource: "fallback", createdAt: Date.now(), cwd, sandbox,
|
|
319
347
|
model: params.model, reasoningEffort: params.reasoningEffort, serviceTier: params.serviceTier };
|
|
320
348
|
this.threads.set(thread.id, thread);
|
|
321
349
|
this.persist();
|
|
322
|
-
this.invalidate(thread.id);
|
|
350
|
+
this.invalidate(thread.id, "thread/started");
|
|
323
351
|
return { backend: "codex", threadId: thread.id, executionMode: mode(sandbox) };
|
|
324
352
|
} finally { this.creating = false; }
|
|
325
353
|
}
|
|
@@ -388,120 +416,18 @@ export class HeadlessBackend {
|
|
|
388
416
|
pagination: { supportsPagination: false, hasPreviousPage: false } } };
|
|
389
417
|
}
|
|
390
418
|
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
attentionOrder(rows, options = { id: "default", promoteOnTurnEnd: true }) {
|
|
401
|
-
if (typeof options.id !== "string" || !options.id || options.id.length > 256
|
|
402
|
-
|| typeof options.promoteOnTurnEnd !== "boolean") throw new Error("Invalid Attention view");
|
|
403
|
-
let view = this.attentionViews.get(options.id);
|
|
404
|
-
if (!view) {
|
|
405
|
-
if (this.attentionViews.size >= 128) throw new Error("Attention view limit reached; release an unused view");
|
|
406
|
-
view = { members: new Map(), nextRank: 1 };
|
|
407
|
-
this.attentionViews.set(options.id, view);
|
|
408
|
-
}
|
|
409
|
-
const members = new Map();
|
|
410
|
-
// Bootstrap oldest turn first. Subsequent ranks follow turn identities and
|
|
411
|
-
// active/terminal transitions, never mutable thread or job activity stamps.
|
|
412
|
-
const eligible = rows.filter((row) => row.threadStatus === "active" || row.inbox.inInbox)
|
|
413
|
-
.sort((a, b) => (Date.parse(this.latest(a.id)?.createdAt) || a.createdAt)
|
|
414
|
-
- (Date.parse(this.latest(b.id)?.createdAt) || b.createdAt) || a.id.localeCompare(b.id));
|
|
415
|
-
for (const row of eligible) {
|
|
416
|
-
const previous = view.members.get(row.id);
|
|
417
|
-
const job = this.latest(row.id);
|
|
418
|
-
const turnKey = job?.id;
|
|
419
|
-
const active = row.threadStatus === "active";
|
|
420
|
-
const newTurn = turnKey !== previous?.turnKey;
|
|
421
|
-
const started = active && (!previous?.active || newTurn);
|
|
422
|
-
const finished = !active && (previous?.active || newTurn);
|
|
423
|
-
const rank = !previous || started || (finished && options.promoteOnTurnEnd) ? view.nextRank++ : previous.rank;
|
|
424
|
-
members.set(row.id, { rank, active, turnKey });
|
|
425
|
-
}
|
|
426
|
-
view.members = members;
|
|
427
|
-
return members;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
query(request) {
|
|
431
|
-
if (request.protocol !== 2 || !request.query || request.inventory === "viewer") throw new Error("Expected owner navigation protocol 2");
|
|
432
|
-
let rows = [...this.threads.values()].filter((thread) => !thread.archived).map((thread) => this.summary(thread));
|
|
433
|
-
const counts = { total: rows.length, active: rows.filter((row) => row.threadStatus === "active").length,
|
|
434
|
-
unread: rows.filter((row) => row.inbox.inInbox).length, review: rows.filter((row) => row.inbox.inInbox && row.threadStatus !== "active").length, pinned: 0 };
|
|
435
|
-
const directories = this.runtime.projects.list().map((project) => {
|
|
436
|
-
const members = rows.filter((row) => row.projectKey === project.path);
|
|
437
|
-
return { key: project.path, kind: "directory", label: project.label, path: project.path,
|
|
438
|
-
counts: { total: members.length, active: members.filter((row) => row.threadStatus === "active").length,
|
|
439
|
-
unread: members.filter((row) => row.inbox.inInbox).length, review: members.filter((row) => row.inbox.inInbox && row.threadStatus !== "active").length, pinned: 0 },
|
|
440
|
-
pinnedRootCount: 0, unpinnedRootCount: members.length, launchpadPresent: true, launchpadBackend: "codex" };
|
|
441
|
-
});
|
|
442
|
-
const query = request.query;
|
|
443
|
-
const attention = query.kind === "lens" && query.lens === "attention" ? this.attentionOrder(rows, request.attentionView) : undefined;
|
|
444
|
-
if (!["exact", "children", "group-members", "directory", "lens", "search", "directory-index", "messaging-projects", "messaging-threads", "model-inventory", "star-map", "star-map-geometry"].includes(query.kind)) throw new Error("Unsupported navigation query");
|
|
445
|
-
if (request.backend && !["codex", "all"].includes(request.backend)) rows = [];
|
|
446
|
-
if (query.kind === "exact") rows = rows.filter((row) => query.identities?.some((ref) => ref.backend === "codex" && ref.threadId === row.id));
|
|
447
|
-
else if (["children", "group-members"].includes(query.kind) || query.roots === "pinned") rows = [];
|
|
448
|
-
else if (query.kind === "directory") rows = rows.filter((row) => row.projectKey === query.directoryKey);
|
|
449
|
-
else if (query.kind === "lens" && query.lens === "attention") rows = rows.filter((row) => row.threadStatus === "active" || row.inbox.inInbox);
|
|
450
|
-
if (query.agentOnly || (query.allowedBackends && !query.allowedBackends.includes("codex"))) rows = [];
|
|
451
|
-
if (query.projectKey) rows = rows.filter((row) => row.projectKey === query.projectKey);
|
|
452
|
-
if (query.directoryKey) rows = rows.filter((row) => row.projectKey === query.directoryKey);
|
|
453
|
-
if (query.kind === "star-map") {
|
|
454
|
-
for (const [key, selection] of Object.entries(query.filters || {})) {
|
|
455
|
-
if (selection === "neutral") continue;
|
|
456
|
-
rows = rows.filter((row) => {
|
|
457
|
-
const matches = key === "attention" && (row.threadStatus === "active" || row.inbox.inInbox);
|
|
458
|
-
return selection === "include" ? matches : !matches;
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
const filter = query.filter || query.text;
|
|
463
|
-
if (filter) rows = rows.filter((row) => row.title.toLowerCase().includes(filter.toLowerCase()));
|
|
464
|
-
const creationOrder = query.kind === "directory" || (query.kind === "lens" && query.lens === "recents");
|
|
465
|
-
rows.sort((a, b) => (attention ? attention.get(b.id).rank - attention.get(a.id).rank
|
|
466
|
-
: creationOrder ? b.createdAt - a.createdAt : b.updatedAt - a.updatedAt) || a.id.localeCompare(b.id));
|
|
467
|
-
const queryKey = digest({ backend: request.backend, query, ...(attention ? { attentionView: request.attentionView } : {}) });
|
|
468
|
-
const generation = digest(attention ? rows.map((row) => ({ ...row, attentionRank: attention.get(row.id).rank })) : rows);
|
|
469
|
-
const countsRevision = digest(counts);
|
|
470
|
-
let offset = 0;
|
|
471
|
-
if (request.cursor) {
|
|
472
|
-
const cursor = JSON.parse(Buffer.from(request.cursor, "base64url").toString());
|
|
473
|
-
if (cursor.queryKey !== queryKey || cursor.generation !== generation || cursor.countsRevision !== countsRevision || cursor.epoch !== this.epoch
|
|
474
|
-
|| !Number.isInteger(cursor.offset) || cursor.offset < 0) throw new Error("Navigation cursor expired; refresh the collection");
|
|
475
|
-
offset = cursor.offset;
|
|
476
|
-
} else if (request.anchor?.kind === "thread") {
|
|
477
|
-
const index = rows.findIndex((row) => row.id === request.anchor.ref.threadId && request.anchor.ref.backend === "codex");
|
|
478
|
-
if (index >= 0) offset = index;
|
|
479
|
-
}
|
|
480
|
-
const limit = Math.min(100, Math.max(1, Number.isInteger(request.pageSize) ? request.pageSize : 50));
|
|
481
|
-
const page = rows.slice(offset, offset + limit);
|
|
482
|
-
const complete = offset + page.length >= rows.length;
|
|
483
|
-
const result = { protocol: 2, queryKey, generation, ownerEpoch: this.epoch, countsRevision, coverage: { state: "complete" }, counts,
|
|
484
|
-
collectionSize: rows.length, rangeStart: offset, complete,
|
|
485
|
-
entries: page.map((row, index) => ({ row: { ...row, ref: { backend: "codex", threadId: row.id }, rowRevision: digest(row),
|
|
486
|
-
ordinaryChildCount: 0, nativeSubAgentGroupPresent: false, queueCount: 0, queueState: "ready" }, orderKey: String(offset + index).padStart(8, "0"),
|
|
487
|
-
...(attention ? { attentionRank: attention.get(row.id).rank } : {}), placement: { kind: "root" } })),
|
|
488
|
-
...(!complete ? { nextCursor: Buffer.from(JSON.stringify({ queryKey, generation, countsRevision, epoch: this.epoch, offset: offset + page.length })).toString("base64url") } : {}) };
|
|
489
|
-
if (["directory-index", "messaging-projects", "star-map-geometry"].includes(query.kind)) {
|
|
490
|
-
result.directories = directories.filter((directory) => (!query.keys || query.keys.includes(directory.key)) && (!query.paths || query.paths.includes(directory.path))
|
|
491
|
-
&& (!query.directoryKey || query.directoryKey === directory.key)
|
|
492
|
-
&& (!query.allowedBackends || query.allowedBackends.includes("codex")) && !query.agentOnly
|
|
493
|
-
&& (!filter || `${directory.label} ${directory.path}`.toLowerCase().includes(filter.toLowerCase())));
|
|
494
|
-
result.collectionSize = result.directories.length;
|
|
495
|
-
result.entries = []; result.complete = true; delete result.nextCursor;
|
|
496
|
-
}
|
|
497
|
-
if (query.kind === "model-inventory") {
|
|
498
|
-
const groups = new Map();
|
|
499
|
-
for (const row of rows) { const key = row.model || ""; groups.set(key, { backend: "codex", model: row.model, threadCount: (groups.get(key)?.threadCount || 0) + 1, fastThreadCount: 0 }); }
|
|
500
|
-
result.modelGroups = [...groups.values()]; result.collectionSize = groups.size;
|
|
501
|
-
result.entries = []; result.complete = true; delete result.nextCursor;
|
|
419
|
+
navigationIndex() {
|
|
420
|
+
const threads = [...this.threads.values()].filter((thread) => !thread.archived).map((thread) => this.summary(thread));
|
|
421
|
+
const byProject = new Map();
|
|
422
|
+
for (const thread of threads) {
|
|
423
|
+
let keys = byProject.get(thread.projectKey);
|
|
424
|
+
if (!keys) { keys = []; byProject.set(thread.projectKey, keys); }
|
|
425
|
+
keys.push(buildThreadIdentityKey(thread.source, thread.id));
|
|
502
426
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
427
|
+
return { threads, directories: this.runtime.projects.list().map((project) => ({
|
|
428
|
+
key: project.path, kind: "directory", label: project.label, path: project.path,
|
|
429
|
+
threadKeys: byProject.get(project.path) || [], needsAttentionCount: 0,
|
|
430
|
+
launchpad: { backend: "codex" },
|
|
431
|
+
})) };
|
|
506
432
|
}
|
|
507
433
|
}
|
package/src/federation-core.ts
CHANGED
|
@@ -23,3 +23,6 @@ export { resolveMessagingOutboundFile, validateMessagingFileMetadata } from "@pw
|
|
|
23
23
|
|
|
24
24
|
export * from "@pwragent/federation/federation-replacement-receiver";
|
|
25
25
|
export { FEDERATION_BACKEND_METHOD_CAPABILITIES } from "@pwragent/federation/federation-backend-methods";
|
|
26
|
+
|
|
27
|
+
export { NavigationQueryStore } from "@pwragent/owner-core/navigation-query-store";
|
|
28
|
+
export { buildThreadIdentityKey, NAVIGATION_QUERY_MAX_RESULT_BYTES } from "@pwragent/shared";
|
package/src/federation.js
CHANGED
|
@@ -142,12 +142,12 @@ export class HeadlessFederation {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
async invite({ port = 47831, label }) {
|
|
145
|
-
if (!Number.isInteger(port) || port <
|
|
145
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error("Federation port must be between 0 and 65535 (0 chooses an available port)");
|
|
146
146
|
this.ensureIdentity(label);
|
|
147
147
|
if (!this.router) this.setupRouter();
|
|
148
|
-
if (this.state.listenPort !== undefined && this.state.listenPort !== port) throw new Error("Restart with the existing listener port; it cannot change while serving");
|
|
148
|
+
if (port !== 0 && this.state.listenPort !== undefined && this.state.listenPort !== port) throw new Error("Restart with the existing listener port; it cannot change while serving");
|
|
149
149
|
await this.listen(port);
|
|
150
|
-
this.state.listenPort = port;
|
|
150
|
+
this.state.listenPort = this.listenerAddress.port;
|
|
151
151
|
const token = randomBytes(32).toString("base64url");
|
|
152
152
|
const now = Date.now();
|
|
153
153
|
const entry = { id: randomUUID(), status: "pending", generatedAt: now, expiresAt: now + 15 * 60_000,
|
package/src/runtime.js
CHANGED
|
@@ -3,12 +3,16 @@ import path from "node:path";
|
|
|
3
3
|
import { readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { AgentToolRouter, buildPwrAgentMessagingToolDefinitions, readPwrAgentMessagingDynamicToolCall, resolveMessagingOutboundFile, validateMessagingFileMetadata, FILE_PUSH_MAX_BYTES } from "../dist/federation-core.js";
|
|
5
5
|
import { Codex } from "./codex.js";
|
|
6
|
+
import { installedVersion } from "./version.js";
|
|
6
7
|
import { ACTIVE, atomicJson, digest, privateDirectory, validId } from "./store.js";
|
|
7
8
|
|
|
8
9
|
export class Runtime {
|
|
9
10
|
constructor({ store, workspace, home, command, onFailure }) {
|
|
10
11
|
this.store = store;
|
|
11
12
|
this.workspace = workspace;
|
|
13
|
+
// Snapshot at startup: upgrading the package files does not change the
|
|
14
|
+
// code this process is running.
|
|
15
|
+
this.running = installedVersion();
|
|
12
16
|
this.projects = new OutpostProjects(path.dirname(store.directory), workspace);
|
|
13
17
|
this.messagingTools = new AgentToolRouter(buildPwrAgentMessagingToolDefinitions((request) => this.messagingRequest(request), { nativeDynamicPdfImages: true }));
|
|
14
18
|
this.fileAccess = { privateStorageRoots: [home, process.env.PWRAGENT_HOME].filter(Boolean),
|
|
@@ -41,7 +45,7 @@ export class Runtime {
|
|
|
41
45
|
async handle(method, params = {}) {
|
|
42
46
|
if (this.stopping) throw new Error("Daemon is stopping");
|
|
43
47
|
switch (method) {
|
|
44
|
-
case "status": return { version: 1, ready: true, pid: process.pid, activeJob: this.active || null, workspace: this.workspace };
|
|
48
|
+
case "status": return { version: 1, ready: true, pid: process.pid, running: this.running, activeJob: this.active || null, workspace: this.workspace };
|
|
45
49
|
case "project-list": return this.projects.list();
|
|
46
50
|
case "project-create": case "project-clone": return this.projects.create(params);
|
|
47
51
|
case "project-add": return this.projects.add(params);
|
package/src/version.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// The release writes pwragentRelease: { tag, commit } into the published
|
|
4
|
+
// manifest. A checkout has none, so only string fields are reported.
|
|
5
|
+
export function versionInfo(pkg) {
|
|
6
|
+
const source = pkg.pwragentRelease;
|
|
7
|
+
const release = {};
|
|
8
|
+
if (typeof source?.tag === "string") release.tag = source.tag;
|
|
9
|
+
if (typeof source?.commit === "string") release.commit = source.commit;
|
|
10
|
+
return { name: pkg.name, version: pkg.version, ...(Object.keys(release).length ? { release } : {}) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Read on each call from this package's own manifest, so the CLI reports the
|
|
14
|
+
// files currently installed. The daemon reads it once at startup instead.
|
|
15
|
+
export function packageVersion() {
|
|
16
|
+
return versionInfo(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The version fields that status compares, without the package name.
|
|
20
|
+
export function installedVersion() {
|
|
21
|
+
const { name: _name, ...fields } = packageVersion();
|
|
22
|
+
return fields;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatVersion({ name, version, release }) {
|
|
26
|
+
const details = [release?.tag, release?.commit && `commit ${release.commit}`].filter(Boolean);
|
|
27
|
+
return details.length ? `${name} ${version} (${details.join(", ")})` : `${name} ${version}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A daemon that predates version reporting returns no running version. It is
|
|
31
|
+
// older than this installation, so it needs a restart too.
|
|
32
|
+
export function restartRequired(running, installed) {
|
|
33
|
+
return !running
|
|
34
|
+
|| running.version !== installed.version
|
|
35
|
+
|| running.release?.commit !== installed.release?.commit;
|
|
36
|
+
}
|