pwragent 1.3.9 → 1.3.10

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 CHANGED
@@ -50,6 +50,19 @@ stop the daemon, install the selected version, and start it again. Do not remove
50
50
  the PwrAgent root, Codex home or workspace during updates. There is no automatic
51
51
  CLI updater or automatic systemd enablement.
52
52
 
53
+ `pwragent --version` prints the installed package version. A published package
54
+ also prints its release tag and source commit, for example
55
+ `pwragent 1.3.8 (v1.3.8, commit 0123…)`. `pwragent version` prints the same
56
+ fields as JSON. Neither needs a running daemon.
57
+
58
+ `pwragent status` reports the daemon's version as `running`, read when that
59
+ daemon started, and the package files as `installed`. `restartRequired` is
60
+ `true` when they differ, so the update has not taken effect yet. A daemon
61
+ started before version reporting has no `running` field and also reports
62
+ `true`. Run `status` from the same installation as the daemon, such as the
63
+ `/opt/pwragent` path in the systemd example; another installation compares its
64
+ own files.
65
+
53
66
  ## systemd installation example
54
67
 
55
68
  On a Linux host with systemd, an administrator must first create the dedicated
@@ -91,7 +104,8 @@ sudo -u pwragent env PWRAGENT_HOME=/var/lib/pwragent PWRAGENT_PROFILE=outpost \
91
104
  For a service update, finish or interrupt active work, run
92
105
  `sudo systemctl stop pwragent-headless.service`, repeat the prefix installation
93
106
  with the chosen new version (or `pwragent@latest`), then run
94
- `sudo systemctl start pwragent-headless.service`. Compare the new example unit
107
+ `sudo systemctl start pwragent-headless.service`. Run the `status` command above
108
+ and check that `restartRequired` is `false`. Compare the new example unit
95
109
  with the installed one before deliberately updating it; npm does not overwrite
96
110
  `/etc/systemd/system`. Stopping during a turn can leave its outcome `unknown`.
97
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pwragent",
3
- "version": "1.3.9",
3
+ "version": "1.3.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Experimental headless PwrAgent daemon and command-line client.",
@@ -53,7 +53,7 @@
53
53
  "devDependencies": {
54
54
  "@pwragent/federation": "workspace:*",
55
55
  "@pwragent/shared": "workspace:*",
56
- "esbuild": "0.28.1",
56
+ "esbuild": "0.28.2",
57
57
  "@pwragent/agent-tools": "workspace:*"
58
58
  },
59
59
  "dependencies": {
@@ -63,7 +63,7 @@
63
63
  "node-pty"
64
64
  ],
65
65
  "pwragentRelease": {
66
- "tag": "v1.3.9",
67
- "commit": "0fc28acd4318e55e172427b5a694c1a1dd096c15"
66
+ "tag": "v1.3.10",
67
+ "commit": "ff9b702144786e162b3e91c78d79791761944350"
68
68
  }
69
69
  }
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 || !positionals.length) { console.log(help); return 0; }
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);
@@ -23,6 +23,11 @@ export function ownerMethodCapabilities(methods, contract = FEDERATION_BACKEND_M
23
23
  }
24
24
  export const METHOD_CAPABILITIES = ownerMethodCapabilities(OWNER_METHODS);
25
25
 
26
+ // The placeholder title desktop gives a new thread. Viewers recognize this
27
+ // exact text as "no name yet".
28
+ export const PLACEHOLDER_THREAD_TITLE = "Untitled thread";
29
+ const LEGACY_PLACEHOLDER_THREAD_TITLE = "New outpost thread";
30
+
26
31
  function mode(sandbox) { return sandbox === "workspace-write" ? "auto" : "default"; }
27
32
  function sandboxFor(params, fallback = "read-only") {
28
33
  if (params.executionMode && !["default", "auto", "full-access"].includes(params.executionMode)) throw new Error("Unsupported execution mode");
@@ -51,7 +56,9 @@ export class HeadlessBackend {
51
56
  try {
52
57
  const data = JSON.parse(readFileSync(this.file, "utf8"));
53
58
  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, thread]));
59
+ this.threads = new Map(data.threads.map((thread) => [thread.id,
60
+ thread.titleSource === "fallback" && thread.title === LEGACY_PLACEHOLDER_THREAD_TITLE
61
+ ? { ...thread, title: PLACEHOLDER_THREAD_TITLE } : thread]));
55
62
  } catch (error) {
56
63
  if (error.code !== "ENOENT") throw error;
57
64
  this.threads = new Map();
@@ -219,13 +226,18 @@ export class HeadlessBackend {
219
226
  for (const key of ["seenAt", "seenUpdatedAt"]) {
220
227
  if (params[key] !== undefined && (!Number.isFinite(params[key]) || params[key] < 0)) throw new Error(`Invalid ${key}`);
221
228
  }
222
- const seenUpdatedAt = params.seenUpdatedAt ?? this.summary(thread).updatedAt;
229
+ // An omitted watermark keeps the stored one (see MarkThreadSeenRequest).
230
+ // A thread never given one is pinned to the seenAt it was judged by,
231
+ // so recording a new seenAt cannot change its unread state.
232
+ thread.seenUpdatedAt = params.seenUpdatedAt ?? thread.seenUpdatedAt ?? thread.seenAt ?? 0;
223
233
  thread.seenAt = params.seenAt ?? Date.now();
224
- thread.seenUpdatedAt = seenUpdatedAt;
225
234
  this.persist();
226
235
  this.forgetSeenAttentionMember(thread.id);
227
- this.invalidate(thread.id);
228
- return { backend: "codex", threadId: thread.id, seenAt: thread.seenAt, seenUpdatedAt };
236
+ this.publish({ backend: "codex", notification: { method: "navigation/thread/seen",
237
+ params: { threadId: thread.id, ...(params.seenUpdatedAt !== undefined ? { seenUpdatedAt: params.seenUpdatedAt } : {}) } } }, "transcript");
238
+ this.invalidate(thread.id, "navigation/thread/seen");
239
+ return { backend: "codex", threadId: thread.id, seenAt: thread.seenAt,
240
+ ...(params.seenUpdatedAt !== undefined ? { seenUpdatedAt: params.seenUpdatedAt } : {}) };
229
241
  }
230
242
  case "setThreadExecutionMode": {
231
243
  const thread = this.owned(params.threadId);
@@ -315,11 +327,11 @@ export class HeadlessBackend {
315
327
  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
328
  if (!result?.thread?.id || typeof result.thread.id !== "string" || result.thread.id.length > 256) throw new Error("Invalid thread/start response");
317
329
  this.runtime.loadedThreads.add(result.thread.id);
318
- const thread = { id: result.thread.id, messagingFileTools: true, title: "New outpost thread", titleSource: "fallback", createdAt: Date.now(), cwd, sandbox,
330
+ const thread = { id: result.thread.id, messagingFileTools: true, title: PLACEHOLDER_THREAD_TITLE, titleSource: "fallback", createdAt: Date.now(), cwd, sandbox,
319
331
  model: params.model, reasoningEffort: params.reasoningEffort, serviceTier: params.serviceTier };
320
332
  this.threads.set(thread.id, thread);
321
333
  this.persist();
322
- this.invalidate(thread.id);
334
+ this.invalidate(thread.id, "thread/started");
323
335
  return { backend: "codex", threadId: thread.id, executionMode: mode(sandbox) };
324
336
  } finally { this.creating = false; }
325
337
  }
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
+ }