pwragent 1.3.8 → 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 +43 -7
- package/dist/federation-core.js +645 -24
- package/package.json +4 -4
- package/src/cli.js +14 -2
- package/src/daemon.js +1 -1
- package/src/federation-backend.js +86 -20
- package/src/federation-core.ts +2 -1
- package/src/handoffs.js +3 -1
- package/src/runtime.js +7 -1
- package/src/version.js +36 -0
package/README.md
CHANGED
|
@@ -26,12 +26,12 @@ Use an exact version to match a desktop installation. See the
|
|
|
26
26
|
and publication verification.
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
npm install --global pwragent@
|
|
29
|
+
npm install --global pwragent@latest
|
|
30
30
|
pwragent --help
|
|
31
|
-
#
|
|
31
|
+
# Or choose a reproducible version that matches your desktop:
|
|
32
|
+
npm install --global pwragent@1.3.7
|
|
33
|
+
# Prerelease desktop versions publish to next, once one has shipped:
|
|
32
34
|
npm install --global pwragent@next
|
|
33
|
-
# Or choose a reproducible version:
|
|
34
|
-
npm install --global pwragent@1.3.6
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Provide **Node >=22 and npm separately**, on Linux (macOS is also supported for
|
|
@@ -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
|
|
@@ -70,7 +83,7 @@ shell's Node version-manager initialization does not run under systemd.
|
|
|
70
83
|
After publication, install a pinned version into the service prefix:
|
|
71
84
|
|
|
72
85
|
```sh
|
|
73
|
-
sudo npm install --prefix /opt/pwragent --omit=dev --ignore-scripts pwragent@1.3.
|
|
86
|
+
sudo npm install --prefix /opt/pwragent --omit=dev --ignore-scripts pwragent@1.3.7
|
|
74
87
|
sudo install -m 0644 /opt/pwragent/node_modules/pwragent/examples/pwragent-headless.service /etc/systemd/system/pwragent-headless.service
|
|
75
88
|
# Review the account, paths, profile and Codex configuration before enabling:
|
|
76
89
|
sudo systemctl daemon-reload
|
|
@@ -90,8 +103,9 @@ sudo -u pwragent env PWRAGENT_HOME=/var/lib/pwragent PWRAGENT_PROFILE=outpost \
|
|
|
90
103
|
|
|
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
|
-
with the chosen new version (or `pwragent@
|
|
94
|
-
`sudo systemctl start pwragent-headless.service`.
|
|
106
|
+
with the chosen new version (or `pwragent@latest`), then run
|
|
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
|
|
|
@@ -280,6 +294,15 @@ its `directoryKey`/`cwd`; omitting it selects the configured workspace root. Sel
|
|
|
280
294
|
the defaults remain local and read-only. Launchpad requests for worktrees,
|
|
281
295
|
grouping, environments or provider extensions are rejected before creation.
|
|
282
296
|
|
|
297
|
+
A new thread is named from its first user message, shortened the way desktop
|
|
298
|
+
names a thread before any helper-model title. The outpost has no helper model.
|
|
299
|
+
The name is stored in `threads.json`, and subscribed viewers receive
|
|
300
|
+
`thread/name/updated` with `titleSource: derived`, followed by a navigation
|
|
301
|
+
invalidation. A later turn never renames a thread. An explicit `renameThread`
|
|
302
|
+
always wins and is never replaced. A thread that still has a placeholder name,
|
|
303
|
+
such as one created before this release, takes Codex's `preview` the next time
|
|
304
|
+
it is read. The outpost does not set the Codex thread name.
|
|
305
|
+
|
|
283
306
|
## Identity, compute lifetime and remaining scope
|
|
284
307
|
|
|
285
308
|
The private `federation.json` stores the retained peer ID, signing/Noise keys,
|
|
@@ -349,6 +372,7 @@ register a project. Enrollment and Git intake do not import the sender's Git or
|
|
|
349
372
|
|
|
350
373
|
| Service | Required grants | Supported behavior |
|
|
351
374
|
| --- | --- | --- |
|
|
375
|
+
| `backend.*` | Per method, from desktop's owner table in `@pwragent/federation` | Navigation reads need `thread_navigation`; thread detail, queue projection, launchpad config and `listBackends` need `thread_detail`; turns need `turn_control`; launchpads need `environment_actions` |
|
|
352
376
|
| `outpost.projects.list` | `thread_navigation` | Owner project inventory |
|
|
353
377
|
| `outpost.projects.create/add/remove` | `environment_actions` | Create/clone, register and unregister projects |
|
|
354
378
|
| `pty.*` | `remote_pty` | Input, resize, ack flow control, snapshots, history and close |
|
|
@@ -465,6 +489,18 @@ against the daemon. External providers and Codex are mocked; no live peers,
|
|
|
465
489
|
operator projects, credentials or compute machines are used. Ubuntu CI runs the
|
|
466
490
|
Node suite; the desktop contract test runs on POSIX lanes.
|
|
467
491
|
|
|
492
|
+
The owner conformance suite
|
|
493
|
+
(`apps/desktop/src/main/__tests__/owner-conformance.test.ts`) runs one thread's
|
|
494
|
+
life through the owner methods both hosts implement. It runs on this daemon and
|
|
495
|
+
on desktop's real owner, and compares each response and event. Each known
|
|
496
|
+
difference is a line in `owner-conformance-baseline.json` beside it, so the
|
|
497
|
+
suite fails on any new difference and names its method. After fixing a
|
|
498
|
+
difference, or changing the contract on purpose, record the result:
|
|
499
|
+
|
|
500
|
+
```sh
|
|
501
|
+
UPDATE_OWNER_CONFORMANCE=1 pnpm test apps/desktop/src/main/__tests__/owner-conformance.test.ts
|
|
502
|
+
```
|
|
503
|
+
|
|
468
504
|
A separate live smoke test installed the packed tarball and official Codex
|
|
469
505
|
0.159.2 in a Node 24 Debian Linux arm64 Docker image. It used an unprivileged
|
|
470
506
|
user, a read-only authentication-file mount, container-owned state and workspace,
|
package/dist/federation-core.js
CHANGED
|
@@ -9284,7 +9284,21 @@ var FEDERATION_CAPABILITIES = [
|
|
|
9284
9284
|
* turn's reply, its images materialized. A messaging gateway without it
|
|
9285
9285
|
* reads a transcript page at every bound turn end.
|
|
9286
9286
|
*/
|
|
9287
|
-
"turn_assistant_reply"
|
|
9287
|
+
"turn_assistant_reply",
|
|
9288
|
+
/**
|
|
9289
|
+
* The owner answers `backend.manageThreadPrWatchesV1`: a viewer reads a
|
|
9290
|
+
* thread's watch history, stops a watch, and arms Watch CI there. Its
|
|
9291
|
+
* navigation rows also carry `prCiWatches`. A viewer shows an older
|
|
9292
|
+
* owner's watches read-only.
|
|
9293
|
+
*/
|
|
9294
|
+
"thread_pr_watch_control_v1",
|
|
9295
|
+
/**
|
|
9296
|
+
* The owner sends each of its Directories projects' Forge Automation on
|
|
9297
|
+
* its rows and answers `backend.setProjectForgeAutomation`,
|
|
9298
|
+
* `backend.listProjectHeldWakeups` and `backend.resolveProjectHeldWakeups`.
|
|
9299
|
+
* A viewer offers no Forge Automation menu items without it.
|
|
9300
|
+
*/
|
|
9301
|
+
"project_forge_automation"
|
|
9288
9302
|
];
|
|
9289
9303
|
var FEDERATION_ENDPOINT_SCHEMES = ["ws", "wss", "ssh"];
|
|
9290
9304
|
function parseFederationGatewayEndpoint(value) {
|
|
@@ -9358,6 +9372,19 @@ var PWRAGENT_MESSAGING_CALLABLE_OPERATION_NAMES = [
|
|
|
9358
9372
|
...PWRAGENT_MESSAGING_LEGACY_OPERATION_NAMES
|
|
9359
9373
|
];
|
|
9360
9374
|
|
|
9375
|
+
// ../shared/src/contracts/thread-todos.ts
|
|
9376
|
+
var THREAD_TODO_TITLE_MAX_LENGTH = 120;
|
|
9377
|
+
var THREAD_TODO_ORIGIN_ID_MAX_LENGTH = 200;
|
|
9378
|
+
function threadTodoMessageOriginFromUnknown(value) {
|
|
9379
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
9380
|
+
const record = value;
|
|
9381
|
+
if (typeof record.id !== "string") return void 0;
|
|
9382
|
+
const id = record.id.trim();
|
|
9383
|
+
if (!id || id.length > THREAD_TODO_ORIGIN_ID_MAX_LENGTH) return void 0;
|
|
9384
|
+
const title = typeof record.title === "string" ? record.title.trim().slice(0, THREAD_TODO_TITLE_MAX_LENGTH).trim() : "";
|
|
9385
|
+
return { id, ...title ? { title } : {} };
|
|
9386
|
+
}
|
|
9387
|
+
|
|
9361
9388
|
// ../shared/src/directory-navigation.ts
|
|
9362
9389
|
function normalizeGitOriginUrl(value) {
|
|
9363
9390
|
const trimmed = value?.trim();
|
|
@@ -9459,6 +9486,61 @@ async function readStatusWithoutNestedWorktrees(options) {
|
|
|
9459
9486
|
return await options.git([...options.statusArgs, ...nestedWorktreePathspecArgs(excludes)]);
|
|
9460
9487
|
}
|
|
9461
9488
|
|
|
9489
|
+
// ../shared/src/thread-titles.ts
|
|
9490
|
+
var MAX_DERIVED_THREAD_TITLE_LENGTH = 72;
|
|
9491
|
+
var MIN_PREFERRED_BREAK_INDEX = 36;
|
|
9492
|
+
var LEADING_SKILL_LINK_RE = /^(?:\[\$[^\]]+\]\([^)]+\)\s*)+/i;
|
|
9493
|
+
var LEADING_SKILL_TOKEN_RE = /^(?:\$[\w:-]+\s+)+/i;
|
|
9494
|
+
var LEADING_REQUEST_PREFIX_RE = /^(?:i need(?: you to)?|can you|could you|would you|please|help me|let's|show me|tell me)\s+/i;
|
|
9495
|
+
function trimTrailingPunctuation(value) {
|
|
9496
|
+
return value.replace(/[\s,;:.-]+$/g, "").trim();
|
|
9497
|
+
}
|
|
9498
|
+
function uppercaseFirstLetter(value) {
|
|
9499
|
+
if (!value) {
|
|
9500
|
+
return value;
|
|
9501
|
+
}
|
|
9502
|
+
return value[0].toUpperCase() + value.slice(1);
|
|
9503
|
+
}
|
|
9504
|
+
function normalizeTitleInput(value) {
|
|
9505
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
9506
|
+
if (!trimmed) {
|
|
9507
|
+
return void 0;
|
|
9508
|
+
}
|
|
9509
|
+
const withoutSkillLinks = trimmed.replace(LEADING_SKILL_LINK_RE, "");
|
|
9510
|
+
const withoutSkillTokens = withoutSkillLinks.replace(LEADING_SKILL_TOKEN_RE, "");
|
|
9511
|
+
return withoutSkillTokens || trimmed;
|
|
9512
|
+
}
|
|
9513
|
+
function shortenDerivedThreadTitle(value) {
|
|
9514
|
+
const normalized = normalizeTitleInput(value);
|
|
9515
|
+
if (!normalized) {
|
|
9516
|
+
return void 0;
|
|
9517
|
+
}
|
|
9518
|
+
const requestTrimmed = normalized.replace(LEADING_REQUEST_PREFIX_RE, "");
|
|
9519
|
+
const candidateBase = requestTrimmed && requestTrimmed.length < normalized.length ? uppercaseFirstLetter(requestTrimmed) : normalized;
|
|
9520
|
+
const candidate = trimTrailingPunctuation(candidateBase);
|
|
9521
|
+
if (candidate.length <= MAX_DERIVED_THREAD_TITLE_LENGTH) {
|
|
9522
|
+
return candidate;
|
|
9523
|
+
}
|
|
9524
|
+
const breakpointWindow = candidate.slice(0, MAX_DERIVED_THREAD_TITLE_LENGTH + 1);
|
|
9525
|
+
const punctuationBreaks = [
|
|
9526
|
+
breakpointWindow.lastIndexOf(". "),
|
|
9527
|
+
breakpointWindow.lastIndexOf("? "),
|
|
9528
|
+
breakpointWindow.lastIndexOf("! "),
|
|
9529
|
+
breakpointWindow.lastIndexOf(": "),
|
|
9530
|
+
breakpointWindow.lastIndexOf("; "),
|
|
9531
|
+
breakpointWindow.lastIndexOf(", ")
|
|
9532
|
+
];
|
|
9533
|
+
const punctuationBreak = Math.max(...punctuationBreaks);
|
|
9534
|
+
if (punctuationBreak >= MIN_PREFERRED_BREAK_INDEX) {
|
|
9535
|
+
return `${trimTrailingPunctuation(candidate.slice(0, punctuationBreak))}...`;
|
|
9536
|
+
}
|
|
9537
|
+
const wordBreak = breakpointWindow.lastIndexOf(" ");
|
|
9538
|
+
if (wordBreak >= MIN_PREFERRED_BREAK_INDEX) {
|
|
9539
|
+
return `${trimTrailingPunctuation(candidate.slice(0, wordBreak))}...`;
|
|
9540
|
+
}
|
|
9541
|
+
return `${trimTrailingPunctuation(candidate.slice(0, MAX_DERIVED_THREAD_TITLE_LENGTH))}...`;
|
|
9542
|
+
}
|
|
9543
|
+
|
|
9462
9544
|
// ../shared/src/messaging-causal-context.ts
|
|
9463
9545
|
function mergeCausalMessagingContext(origins) {
|
|
9464
9546
|
const sources = origins.flatMap((origin) => origin?.causalMessagingSource ? [origin.causalMessagingSource] : []);
|
|
@@ -9468,6 +9550,215 @@ function mergeCausalMessagingContext(origins) {
|
|
|
9468
9550
|
return ambiguous ? { causalMessagingSource: void 0, causalMessagingAmbiguous: true } : { causalMessagingSource: sources[sources.length - 1], causalMessagingAmbiguous: void 0 };
|
|
9469
9551
|
}
|
|
9470
9552
|
|
|
9553
|
+
// ../shared/src/thread-replay-paging.ts
|
|
9554
|
+
function pageThreadReplay(replay, options, hooks = {}) {
|
|
9555
|
+
if (options.includeTurns === false) {
|
|
9556
|
+
return {
|
|
9557
|
+
...replay,
|
|
9558
|
+
entries: [],
|
|
9559
|
+
messages: [],
|
|
9560
|
+
lastUserMessage: void 0,
|
|
9561
|
+
lastAssistantMessage: void 0,
|
|
9562
|
+
pagination: {
|
|
9563
|
+
supportsPagination: false,
|
|
9564
|
+
hasPreviousPage: false
|
|
9565
|
+
}
|
|
9566
|
+
};
|
|
9567
|
+
}
|
|
9568
|
+
if (options.limit === void 0 && options.before === void 0) {
|
|
9569
|
+
return replay;
|
|
9570
|
+
}
|
|
9571
|
+
const beforeIndex = options.before !== void 0 ? replay.entries.findIndex((entry) => entry.id === options.before) : replay.entries.length;
|
|
9572
|
+
if (beforeIndex < 0) {
|
|
9573
|
+
hooks.onUnresolvedCursor?.();
|
|
9574
|
+
return {
|
|
9575
|
+
...replay,
|
|
9576
|
+
entries: [],
|
|
9577
|
+
messages: [],
|
|
9578
|
+
lastUserMessage: void 0,
|
|
9579
|
+
lastAssistantMessage: void 0,
|
|
9580
|
+
pagination: {
|
|
9581
|
+
supportsPagination: true,
|
|
9582
|
+
hasPreviousPage: false
|
|
9583
|
+
}
|
|
9584
|
+
};
|
|
9585
|
+
}
|
|
9586
|
+
const limit = options.limit === void 0 ? void 0 : Math.max(0, Math.floor(options.limit));
|
|
9587
|
+
const startIndex = limit === void 0 ? 0 : findTurnPageStartIndex(replay.entries, beforeIndex, limit);
|
|
9588
|
+
const entries = replay.entries.slice(startIndex, beforeIndex);
|
|
9589
|
+
return replayWithEntries(
|
|
9590
|
+
replay,
|
|
9591
|
+
entries,
|
|
9592
|
+
mintEntryCursorPagination(entries, startIndex > 0, hooks.isCursorEntry)
|
|
9593
|
+
);
|
|
9594
|
+
}
|
|
9595
|
+
function findTurnPageStartIndex(entries, endIndex, limit) {
|
|
9596
|
+
if (limit === 0) {
|
|
9597
|
+
return endIndex;
|
|
9598
|
+
}
|
|
9599
|
+
for (let index = 0; index < endIndex; index += 1) {
|
|
9600
|
+
if (!entries[index]?.turn?.id) {
|
|
9601
|
+
return Math.max(0, endIndex - limit);
|
|
9602
|
+
}
|
|
9603
|
+
}
|
|
9604
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
9605
|
+
let oldestRetainedTurnId;
|
|
9606
|
+
for (let index = endIndex - 1; index >= 0; index -= 1) {
|
|
9607
|
+
const turnId = entries[index]?.turn?.id;
|
|
9608
|
+
if (!turnId || turnIds.has(turnId)) {
|
|
9609
|
+
continue;
|
|
9610
|
+
}
|
|
9611
|
+
if (turnIds.size >= limit) {
|
|
9612
|
+
break;
|
|
9613
|
+
}
|
|
9614
|
+
turnIds.add(turnId);
|
|
9615
|
+
oldestRetainedTurnId = turnId;
|
|
9616
|
+
}
|
|
9617
|
+
if (!oldestRetainedTurnId) {
|
|
9618
|
+
return Math.max(0, endIndex - limit);
|
|
9619
|
+
}
|
|
9620
|
+
if (turnIds.size < limit) {
|
|
9621
|
+
return 0;
|
|
9622
|
+
}
|
|
9623
|
+
const firstTurnEntryIndex = entries.findIndex(
|
|
9624
|
+
(entry, index) => index < endIndex && entry.turn?.id === oldestRetainedTurnId
|
|
9625
|
+
);
|
|
9626
|
+
return firstTurnEntryIndex >= 0 ? firstTurnEntryIndex : 0;
|
|
9627
|
+
}
|
|
9628
|
+
function mintEntryCursorPagination(entries, hasPreviousPage, isCursorEntry = () => true) {
|
|
9629
|
+
const cursorEntry = hasPreviousPage ? entries.find(isCursorEntry) : void 0;
|
|
9630
|
+
return {
|
|
9631
|
+
supportsPagination: true,
|
|
9632
|
+
hasPreviousPage: cursorEntry !== void 0,
|
|
9633
|
+
...cursorEntry ? { previousCursor: cursorEntry.id } : {}
|
|
9634
|
+
};
|
|
9635
|
+
}
|
|
9636
|
+
function replayWithEntries(replay, entries, pagination) {
|
|
9637
|
+
const messages = entries.flatMap(
|
|
9638
|
+
(entry) => entry.type === "message" ? [
|
|
9639
|
+
{
|
|
9640
|
+
id: entry.id,
|
|
9641
|
+
role: entry.role,
|
|
9642
|
+
text: entry.text,
|
|
9643
|
+
...entry.parts ? { parts: entry.parts } : {},
|
|
9644
|
+
...entry.origin ? { origin: entry.origin } : {},
|
|
9645
|
+
...entry.createdAt ? { createdAt: entry.createdAt } : {}
|
|
9646
|
+
}
|
|
9647
|
+
] : []
|
|
9648
|
+
);
|
|
9649
|
+
const lastUserMessage = messages.findLast((message) => message.role === "user")?.text;
|
|
9650
|
+
const lastAssistantMessage = messages.findLast(
|
|
9651
|
+
(message) => message.role === "assistant"
|
|
9652
|
+
)?.text;
|
|
9653
|
+
return {
|
|
9654
|
+
...replay,
|
|
9655
|
+
entries,
|
|
9656
|
+
messages,
|
|
9657
|
+
lastUserMessage,
|
|
9658
|
+
lastAssistantMessage,
|
|
9659
|
+
pagination
|
|
9660
|
+
};
|
|
9661
|
+
}
|
|
9662
|
+
var FEDERATION_OMITTED_ENTRY_TEXT = "Content omitted because this page exceeded the federation frame limit.";
|
|
9663
|
+
function fitThreadReplayWithinByteBudget(params) {
|
|
9664
|
+
if (params.measureBytes(params.replay) <= params.maxBytes) {
|
|
9665
|
+
return params.replay;
|
|
9666
|
+
}
|
|
9667
|
+
if (params.cursorIdSpace === "provider-cursor") {
|
|
9668
|
+
return compactReplayWithinByteBudget(params);
|
|
9669
|
+
}
|
|
9670
|
+
let entries = params.replay.entries;
|
|
9671
|
+
while (entries.length > 1) {
|
|
9672
|
+
entries = entries.slice(1);
|
|
9673
|
+
const candidate = replayWithEntries(
|
|
9674
|
+
params.replay,
|
|
9675
|
+
entries,
|
|
9676
|
+
mintEntryCursorPagination(entries, true, params.isCursorEntry)
|
|
9677
|
+
);
|
|
9678
|
+
if (params.measureBytes(candidate) <= params.maxBytes) {
|
|
9679
|
+
return candidate;
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
9682
|
+
const oversizedEntry = entries[0];
|
|
9683
|
+
if (oversizedEntry) {
|
|
9684
|
+
const candidate = replayWithEntries(
|
|
9685
|
+
params.replay,
|
|
9686
|
+
[compactOversizedEntry(oversizedEntry)],
|
|
9687
|
+
// Classify the entry as it arrived. Compaction rewrites an activity's
|
|
9688
|
+
// summary, and a usage row matched by summary rather than by id would
|
|
9689
|
+
// read as provider-owned afterwards — the overlay-id cursor this module
|
|
9690
|
+
// exists to prevent.
|
|
9691
|
+
mintEntryCursorPagination(
|
|
9692
|
+
[oversizedEntry],
|
|
9693
|
+
params.replay.pagination.hasPreviousPage || params.replay.entries.length > 1,
|
|
9694
|
+
params.isCursorEntry
|
|
9695
|
+
)
|
|
9696
|
+
);
|
|
9697
|
+
if (params.measureBytes(candidate) <= params.maxBytes) {
|
|
9698
|
+
return candidate;
|
|
9699
|
+
}
|
|
9700
|
+
}
|
|
9701
|
+
return replayWithEntries(params.replay, [], mintEntryCursorPagination([], false));
|
|
9702
|
+
}
|
|
9703
|
+
function compactReplayWithinByteBudget(params) {
|
|
9704
|
+
const entries = [...params.replay.entries];
|
|
9705
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
9706
|
+
entries[index] = compactOversizedEntry(entries[index]);
|
|
9707
|
+
const candidate = replayWithEntries(
|
|
9708
|
+
params.replay,
|
|
9709
|
+
[...entries],
|
|
9710
|
+
params.replay.pagination
|
|
9711
|
+
);
|
|
9712
|
+
if (params.measureBytes(candidate) <= params.maxBytes) {
|
|
9713
|
+
return candidate;
|
|
9714
|
+
}
|
|
9715
|
+
}
|
|
9716
|
+
return replayWithEntries(params.replay, [], params.replay.pagination);
|
|
9717
|
+
}
|
|
9718
|
+
function compactOversizedEntry(entry) {
|
|
9719
|
+
switch (entry.type) {
|
|
9720
|
+
case "message":
|
|
9721
|
+
return {
|
|
9722
|
+
type: "message",
|
|
9723
|
+
id: entry.id,
|
|
9724
|
+
role: entry.role,
|
|
9725
|
+
text: FEDERATION_OMITTED_ENTRY_TEXT,
|
|
9726
|
+
...entry.createdAt ? { createdAt: entry.createdAt } : {},
|
|
9727
|
+
...entry.phase ? { phase: entry.phase } : {},
|
|
9728
|
+
...entry.turn ? { turn: entry.turn } : {}
|
|
9729
|
+
};
|
|
9730
|
+
case "activity":
|
|
9731
|
+
return {
|
|
9732
|
+
type: "activity",
|
|
9733
|
+
id: entry.id,
|
|
9734
|
+
summary: FEDERATION_OMITTED_ENTRY_TEXT,
|
|
9735
|
+
details: [],
|
|
9736
|
+
...entry.createdAt ? { createdAt: entry.createdAt } : {},
|
|
9737
|
+
...entry.tone ? { tone: entry.tone } : {},
|
|
9738
|
+
...entry.status ? { status: entry.status } : {},
|
|
9739
|
+
...entry.turn ? { turn: entry.turn } : {}
|
|
9740
|
+
};
|
|
9741
|
+
case "plan":
|
|
9742
|
+
return {
|
|
9743
|
+
type: "plan",
|
|
9744
|
+
id: entry.id,
|
|
9745
|
+
markdown: FEDERATION_OMITTED_ENTRY_TEXT,
|
|
9746
|
+
steps: [],
|
|
9747
|
+
...entry.createdAt ? { createdAt: entry.createdAt } : {},
|
|
9748
|
+
...entry.turn ? { turn: entry.turn } : {}
|
|
9749
|
+
};
|
|
9750
|
+
case "review":
|
|
9751
|
+
return {
|
|
9752
|
+
type: "review",
|
|
9753
|
+
id: entry.id,
|
|
9754
|
+
review: FEDERATION_OMITTED_ENTRY_TEXT,
|
|
9755
|
+
...entry.createdAt ? { createdAt: entry.createdAt } : {},
|
|
9756
|
+
...entry.status ? { status: entry.status } : {},
|
|
9757
|
+
...entry.turn ? { turn: entry.turn } : {}
|
|
9758
|
+
};
|
|
9759
|
+
}
|
|
9760
|
+
}
|
|
9761
|
+
|
|
9471
9762
|
// ../federation/src/federation-identity.ts
|
|
9472
9763
|
import {
|
|
9473
9764
|
createPrivateKey,
|
|
@@ -12427,6 +12718,9 @@ var TerminalReplay = class {
|
|
|
12427
12718
|
parsedTail = "";
|
|
12428
12719
|
seq = 0;
|
|
12429
12720
|
updatedAt = 0;
|
|
12721
|
+
/** The last serialized screen, until a write or resize changes it. A thread
|
|
12722
|
+
* view remount attaches every pane at once, and most panes are idle. */
|
|
12723
|
+
serialized;
|
|
12430
12724
|
pendingCharacters = 0;
|
|
12431
12725
|
whenDrained() {
|
|
12432
12726
|
return this.queue;
|
|
@@ -12456,6 +12750,7 @@ var TerminalReplay = class {
|
|
|
12456
12750
|
}
|
|
12457
12751
|
this.queue = this.queue.then(() => new Promise((resolve) => {
|
|
12458
12752
|
this.terminal.write(data, () => {
|
|
12753
|
+
this.serialized = void 0;
|
|
12459
12754
|
this.pendingCharacters -= data.length;
|
|
12460
12755
|
this.parsedTail = tail;
|
|
12461
12756
|
this.parsedSeq = frame.seq;
|
|
@@ -12467,34 +12762,42 @@ var TerminalReplay = class {
|
|
|
12467
12762
|
}
|
|
12468
12763
|
resize(cols, rows) {
|
|
12469
12764
|
this.queue = this.queue.then(() => {
|
|
12765
|
+
const { cols: beforeCols, rows: beforeRows } = this.terminal;
|
|
12470
12766
|
this.terminal.resize(clampTerminalColumns(cols), clampTerminalRows(rows));
|
|
12767
|
+
if (this.terminal.cols !== beforeCols || this.terminal.rows !== beforeRows) this.serialized = void 0;
|
|
12471
12768
|
});
|
|
12472
12769
|
}
|
|
12473
12770
|
snapshot() {
|
|
12474
12771
|
const result = this.queue.then(() => {
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
while (Buffer.byteLength(data) > TERMINAL_SNAPSHOT_LIMIT && scrollback > 0) {
|
|
12478
|
-
scrollback = Math.floor(scrollback / 2);
|
|
12479
|
-
data = this.serializer.serialize({ scrollback });
|
|
12480
|
-
}
|
|
12481
|
-
const truncated = scrollback < 2e3;
|
|
12482
|
-
if (Buffer.byteLength(data) > TERMINAL_SNAPSHOT_LIMIT) {
|
|
12483
|
-
data = this.plainText().slice(-64 * 1024).replace(/\n/g, "\r\n");
|
|
12484
|
-
}
|
|
12485
|
-
return {
|
|
12486
|
-
data,
|
|
12487
|
-
pendingEscape: this.parsedTail || void 0,
|
|
12488
|
-
seq: this.parsedSeq,
|
|
12489
|
-
timestamp: this.updatedAt,
|
|
12490
|
-
cols: this.terminal.cols,
|
|
12491
|
-
rows: this.terminal.rows,
|
|
12492
|
-
truncated
|
|
12493
|
-
};
|
|
12772
|
+
this.serialized ??= this.serialize();
|
|
12773
|
+
return { ...this.serialized };
|
|
12494
12774
|
});
|
|
12495
|
-
this.queue = result.then(() => void 0);
|
|
12775
|
+
this.queue = result.then(() => void 0, () => void 0);
|
|
12496
12776
|
return result;
|
|
12497
12777
|
}
|
|
12778
|
+
serialize() {
|
|
12779
|
+
let scrollback = 2e3;
|
|
12780
|
+
let data = this.serializer.serialize({ scrollback });
|
|
12781
|
+
let bytes = Buffer.byteLength(data);
|
|
12782
|
+
while (bytes > TERMINAL_SNAPSHOT_LIMIT && scrollback > 0) {
|
|
12783
|
+
scrollback = Math.floor(scrollback / 2);
|
|
12784
|
+
data = this.serializer.serialize({ scrollback });
|
|
12785
|
+
bytes = Buffer.byteLength(data);
|
|
12786
|
+
}
|
|
12787
|
+
const truncated = scrollback < 2e3;
|
|
12788
|
+
if (bytes > TERMINAL_SNAPSHOT_LIMIT) {
|
|
12789
|
+
data = this.plainText().slice(-64 * 1024).replace(/\n/g, "\r\n");
|
|
12790
|
+
}
|
|
12791
|
+
return {
|
|
12792
|
+
data,
|
|
12793
|
+
pendingEscape: this.parsedTail || void 0,
|
|
12794
|
+
seq: this.parsedSeq,
|
|
12795
|
+
timestamp: this.updatedAt,
|
|
12796
|
+
cols: this.terminal.cols,
|
|
12797
|
+
rows: this.terminal.rows,
|
|
12798
|
+
truncated
|
|
12799
|
+
};
|
|
12800
|
+
}
|
|
12498
12801
|
/** Raw reads are event deltas. Plain reads are screen snapshots, not deltas. */
|
|
12499
12802
|
read(afterSeq = 0, maxChars = 16384) {
|
|
12500
12803
|
const limit = Math.max(1, Math.min(64 * 1024, Math.floor(maxChars) || 16384));
|
|
@@ -12523,7 +12826,7 @@ var TerminalReplay = class {
|
|
|
12523
12826
|
seq: this.parsedSeq,
|
|
12524
12827
|
timestamp: this.updatedAt
|
|
12525
12828
|
}));
|
|
12526
|
-
this.queue = result.then(() => void 0);
|
|
12829
|
+
this.queue = result.then(() => void 0, () => void 0);
|
|
12527
12830
|
return result;
|
|
12528
12831
|
}
|
|
12529
12832
|
plainText() {
|
|
@@ -18945,15 +19248,20 @@ var FederationFilePushReceipts = class {
|
|
|
18945
19248
|
function authenticateMessageOrigin(params) {
|
|
18946
19249
|
const sourceThread = params.messageOrigin?.sourceThread;
|
|
18947
19250
|
if (!params.messageOrigin || !sourceThread) {
|
|
18948
|
-
return params.messageOrigin;
|
|
19251
|
+
if (params.messageOrigin?.threadTodo === void 0) return params.messageOrigin;
|
|
19252
|
+
const { threadTodo: _threadTodo, ...messageOrigin2 } = params.messageOrigin;
|
|
19253
|
+
return messageOrigin2;
|
|
18949
19254
|
}
|
|
18950
19255
|
let sourceInstance;
|
|
18951
19256
|
try {
|
|
18952
19257
|
sourceInstance = params.resolveSourceInstance?.(params.sourceInstanceId);
|
|
18953
19258
|
} catch {
|
|
18954
19259
|
}
|
|
19260
|
+
const { threadTodo: rawThreadTodo, ...messageOrigin } = params.messageOrigin;
|
|
19261
|
+
const threadTodo = messageOrigin.kind === "agent" ? threadTodoMessageOriginFromUnknown(rawThreadTodo) : void 0;
|
|
18955
19262
|
return {
|
|
18956
|
-
...
|
|
19263
|
+
...messageOrigin,
|
|
19264
|
+
...threadTodo ? { threadTodo } : {},
|
|
18957
19265
|
sourceThread: {
|
|
18958
19266
|
backend: sourceThread.backend,
|
|
18959
19267
|
instanceId: params.sourceInstanceId,
|
|
@@ -19486,8 +19794,318 @@ var FederationReplacementReceiver = class {
|
|
|
19486
19794
|
return pending.entries;
|
|
19487
19795
|
}
|
|
19488
19796
|
};
|
|
19797
|
+
|
|
19798
|
+
// ../federation/src/federation-backend-methods.ts
|
|
19799
|
+
var FEDERATION_BACKEND_METHODS = {
|
|
19800
|
+
getNavigationQueryPage: "backend.getNavigationQueryPage",
|
|
19801
|
+
removeNavigationDirectory: "backend.removeNavigationDirectory",
|
|
19802
|
+
archiveNavigationDirectory: "backend.archiveNavigationDirectory",
|
|
19803
|
+
markNavigationDirectorySeen: "backend.markNavigationDirectorySeen",
|
|
19804
|
+
releaseNavigationAttentionView: "backend.releaseNavigationAttentionView",
|
|
19805
|
+
getNavigationLaunchpadConfig: "backend.getNavigationLaunchpadConfig",
|
|
19806
|
+
getNavigationSelectedDetail: "backend.getNavigationSelectedDetail",
|
|
19807
|
+
getNavigationQueueProjection: "backend.getNavigationQueueProjection",
|
|
19808
|
+
getProjectPage: "backend.getProjectPage",
|
|
19809
|
+
getNavigationDescendantPage: "backend.getNavigationDescendantPage",
|
|
19810
|
+
lookupArchivedThreads: "backend.lookupArchivedThreads",
|
|
19811
|
+
/** @deprecated Alpha collection contract; migrate per docs/federation-navigation-v2.md before removal. */
|
|
19812
|
+
getNavigationSnapshot: "backend.getNavigationSnapshot",
|
|
19813
|
+
searchNavigationThreads: "backend.searchNavigationThreads",
|
|
19814
|
+
searchFederatedThreads: "backend.searchFederatedThreads",
|
|
19815
|
+
searchThreadContent: "backend.searchThreadContent",
|
|
19816
|
+
cancelThreadContentSearch: "backend.cancelThreadContentSearch",
|
|
19817
|
+
/** @deprecated Collection enumeration; use bounded queries or exact-ID archive lookup. */
|
|
19818
|
+
listThreads: "backend.listThreads",
|
|
19819
|
+
resolveThread: "backend.resolveThread",
|
|
19820
|
+
resolveThreadAdmissionState: "backend.resolveThreadAdmissionState",
|
|
19821
|
+
readThread: "backend.readThread",
|
|
19822
|
+
readTurnAssistantReply: "backend.readTurnAssistantReply",
|
|
19823
|
+
readUsageActivity: "backend.readUsageActivity",
|
|
19824
|
+
readThreadFamilyPricing: "backend.readThreadFamilyPricing",
|
|
19825
|
+
analyzeUsageActivity: "backend.analyzeUsageActivity",
|
|
19826
|
+
inspectTokenMiserOutput: "backend.inspectTokenMiserOutput",
|
|
19827
|
+
analyzeThreadToolHistory: "backend.analyzeThreadToolHistory",
|
|
19828
|
+
readTranscriptImage: "backend.readTranscriptImage",
|
|
19829
|
+
listSkills: "backend.listSkills",
|
|
19830
|
+
supportsMessagingPdfTools: "backend.supportsMessagingPdfTools",
|
|
19831
|
+
listBackends: "backend.listBackends",
|
|
19832
|
+
markThreadSeen: "backend.markThreadSeen",
|
|
19833
|
+
continueThreadEnvironmentStartup: "backend.continueThreadEnvironmentStartup",
|
|
19834
|
+
acknowledgeThreadEnvironmentFailure: "backend.acknowledgeThreadEnvironmentFailure",
|
|
19835
|
+
setThreadReaction: "backend.setThreadReaction",
|
|
19836
|
+
setThreadLock: "backend.setThreadLock",
|
|
19837
|
+
setThreadPin: "backend.setThreadPin",
|
|
19838
|
+
reorderThreadPins: "backend.reorderThreadPins",
|
|
19839
|
+
mountRemoteChild: "backend.mountRemoteChild",
|
|
19840
|
+
setThreadParent: "backend.setThreadParent",
|
|
19841
|
+
mutateThreadParentV1: "backend.mutateThreadParentV1",
|
|
19842
|
+
updateSubthreadOrder: "backend.updateSubthreadOrder",
|
|
19843
|
+
setSubthreadsCollapsed: "backend.setSubthreadsCollapsed",
|
|
19844
|
+
readForge: "backend.readForge",
|
|
19845
|
+
detachThreadPullRequest: "backend.detachThreadPullRequest",
|
|
19846
|
+
setThreadPrAutoDispatch: "backend.setThreadPrAutoDispatch",
|
|
19847
|
+
cancelThreadPrAutoDispatch: "backend.cancelThreadPrAutoDispatch",
|
|
19848
|
+
sendThreadPrAutoDispatchNow: "backend.sendThreadPrAutoDispatchNow",
|
|
19849
|
+
// A new method, so an owner that predates it answers method_not_found.
|
|
19850
|
+
manageThreadPrWatchesV1: "backend.manageThreadPrWatchesV1",
|
|
19851
|
+
setProjectForgeAutomation: "backend.setProjectForgeAutomation",
|
|
19852
|
+
listProjectHeldWakeups: "backend.listProjectHeldWakeups",
|
|
19853
|
+
resolveProjectHeldWakeups: "backend.resolveProjectHeldWakeups",
|
|
19854
|
+
archiveThread: "backend.archiveThread",
|
|
19855
|
+
restoreThread: "backend.restoreThread",
|
|
19856
|
+
startThread: "backend.startThread",
|
|
19857
|
+
forkThread: "backend.forkThread",
|
|
19858
|
+
startTurn: "backend.startTurn",
|
|
19859
|
+
submitTurnRequest: "backend.submitTurnRequest",
|
|
19860
|
+
getTurnRequest: "backend.getTurnRequest",
|
|
19861
|
+
submitSteerRequest: "backend.submitSteerRequest",
|
|
19862
|
+
getSteerRequest: "backend.getSteerRequest",
|
|
19863
|
+
replaceQueuedMessage: "backend.replaceQueuedMessage",
|
|
19864
|
+
readQueuedTurn: "backend.readQueuedTurn",
|
|
19865
|
+
cancelQueuedTurn: "backend.cancelQueuedTurn",
|
|
19866
|
+
releaseQueuedTurn: "backend.releaseQueuedTurn",
|
|
19867
|
+
moveQueuedTurn: "backend.moveQueuedTurn",
|
|
19868
|
+
setQueuedTurnPaused: "backend.setQueuedTurnPaused",
|
|
19869
|
+
startReview: "backend.startReview",
|
|
19870
|
+
startPullRequestReview: "backend.startPullRequestReview",
|
|
19871
|
+
listScheduledThreadActions: "backend.listScheduledThreadActions",
|
|
19872
|
+
createScheduledThreadAction: "backend.createScheduledThreadAction",
|
|
19873
|
+
createScheduledPullRequestReview: "backend.createScheduledPullRequestReview",
|
|
19874
|
+
updateScheduledThreadAction: "backend.updateScheduledThreadAction",
|
|
19875
|
+
updateScheduledPullRequestReview: "backend.updateScheduledPullRequestReview",
|
|
19876
|
+
cancelScheduledThreadAction: "backend.cancelScheduledThreadAction",
|
|
19877
|
+
sendScheduledThreadActionNow: "backend.sendScheduledThreadActionNow",
|
|
19878
|
+
compactThread: "backend.compactThread",
|
|
19879
|
+
rewindThread: "backend.rewindThread",
|
|
19880
|
+
previewThreadRewind: "backend.previewThreadRewind",
|
|
19881
|
+
undoThreadRewind: "backend.undoThreadRewind",
|
|
19882
|
+
listMcpConnections: "backend.listMcpConnections",
|
|
19883
|
+
readThreadMcpConnections: "backend.readThreadMcpConnections",
|
|
19884
|
+
setThreadMcpConnections: "backend.setThreadMcpConnections",
|
|
19885
|
+
describeThreadMcpConnections: "backend.describeThreadMcpConnections",
|
|
19886
|
+
listThreadMcpServers: "backend.listThreadMcpServers",
|
|
19887
|
+
reloadCodexMcpConfig: "backend.reloadCodexMcpConfig",
|
|
19888
|
+
controlActiveTurn: "backend.controlActiveTurn",
|
|
19889
|
+
resolveActiveTurn: "backend.resolveActiveTurn",
|
|
19890
|
+
interruptTurn: "backend.interruptTurn",
|
|
19891
|
+
stopSubAgent: "backend.stopSubAgent",
|
|
19892
|
+
steerTurn: "backend.steerTurn",
|
|
19893
|
+
setThreadExecutionMode: "backend.setThreadExecutionMode",
|
|
19894
|
+
queueThreadExecutionMode: "backend.queueThreadExecutionMode",
|
|
19895
|
+
cancelThreadExecutionModeQueue: "backend.cancelThreadExecutionModeQueue",
|
|
19896
|
+
setAcpSessionRuntimeOption: "backend.setAcpSessionRuntimeOption",
|
|
19897
|
+
setThreadAgent: "backend.setThreadAgent",
|
|
19898
|
+
setThreadTokenMiser: "backend.setThreadTokenMiser",
|
|
19899
|
+
setThreadModelSettings: "backend.setThreadModelSettings",
|
|
19900
|
+
checkThreadBranchDrift: "backend.checkThreadBranchDrift",
|
|
19901
|
+
updateThreadExpectedBranch: "backend.updateThreadExpectedBranch",
|
|
19902
|
+
retainThreadBranchDrift: "backend.retainThreadBranchDrift",
|
|
19903
|
+
submitServerRequest: "backend.submitServerRequest",
|
|
19904
|
+
runCodexEnvironmentAction: "backend.runCodexEnvironmentAction",
|
|
19905
|
+
stopCodexEnvironmentAction: "backend.stopCodexEnvironmentAction",
|
|
19906
|
+
listBackgroundTerminals: "backend.listBackgroundTerminals",
|
|
19907
|
+
terminateBackgroundTerminal: "backend.terminateBackgroundTerminal",
|
|
19908
|
+
setCodexThreadEnvironment: "backend.setCodexThreadEnvironment",
|
|
19909
|
+
refreshThreadPullRequests: "backend.refreshThreadPullRequests",
|
|
19910
|
+
refreshDirectoryGitStatuses: "backend.refreshDirectoryGitStatuses",
|
|
19911
|
+
refreshThreadGitWorkingState: "backend.refreshThreadGitWorkingState",
|
|
19912
|
+
ensureDirectoryLaunchpad: "backend.ensureDirectoryLaunchpad",
|
|
19913
|
+
listRecentFileReferences: "backend.listRecentFileReferences",
|
|
19914
|
+
recordRecentFileReferences: "backend.recordRecentFileReferences",
|
|
19915
|
+
listModelSettingsRecents: "backend.listModelSettingsRecents",
|
|
19916
|
+
recordModelSettingsRecent: "backend.recordModelSettingsRecent",
|
|
19917
|
+
attachDirectoryToThread: "backend.attachDirectoryToThread",
|
|
19918
|
+
detachDirectoryFromThread: "backend.detachDirectoryFromThread",
|
|
19919
|
+
listWorktreeUnpublishedCommits: "backend.listWorktreeUnpublishedCommits",
|
|
19920
|
+
getWorktreeUnpublishedCommitDiff: "backend.getWorktreeUnpublishedCommitDiff",
|
|
19921
|
+
materializeDirectoryLaunchpad: "backend.materializeDirectoryLaunchpad",
|
|
19922
|
+
manageLaunchpadCreation: "backend.manageLaunchpadCreation",
|
|
19923
|
+
handoffThreadWorkspace: "backend.handoffThreadWorkspace",
|
|
19924
|
+
// Separate so an owner that predates these moves answers method_not_found.
|
|
19925
|
+
// It would read an unknown direction as Handoff to Local and ignore baseRef.
|
|
19926
|
+
handoffThreadWorkspaceToNewWorktree: "backend.handoffThreadWorkspaceToNewWorktree",
|
|
19927
|
+
recreateThreadWorktree: "backend.recreateThreadWorktree",
|
|
19928
|
+
moveThreadToMainCheckout: "backend.moveThreadToMainCheckout",
|
|
19929
|
+
renameThread: "backend.renameThread",
|
|
19930
|
+
readApplications: "backend.readApplications",
|
|
19931
|
+
openApplication: "backend.openApplication",
|
|
19932
|
+
readMessagingPlatformStatuses: "backend.readMessagingPlatformStatuses",
|
|
19933
|
+
readPwrSnapConnectionStatus: "backend.readPwrSnapConnectionStatus",
|
|
19934
|
+
getLoadStatus: "backend.getLoadStatus",
|
|
19935
|
+
trustCodexProject: "backend.trustCodexProject",
|
|
19936
|
+
setCelestialIcon: "backend.setCelestialIcon",
|
|
19937
|
+
setFederationShortName: "backend.setFederationShortName",
|
|
19938
|
+
starMapIntake: "backend.starMapIntake"
|
|
19939
|
+
};
|
|
19940
|
+
var FEDERATION_BACKEND_METHOD_CAPABILITIES = {
|
|
19941
|
+
[FEDERATION_BACKEND_METHODS.getNavigationQueryPage]: "thread_navigation",
|
|
19942
|
+
[FEDERATION_BACKEND_METHODS.removeNavigationDirectory]: "thread_navigation",
|
|
19943
|
+
[FEDERATION_BACKEND_METHODS.archiveNavigationDirectory]: "turn_control",
|
|
19944
|
+
[FEDERATION_BACKEND_METHODS.markNavigationDirectorySeen]: "thread_navigation",
|
|
19945
|
+
[FEDERATION_BACKEND_METHODS.releaseNavigationAttentionView]: "thread_navigation",
|
|
19946
|
+
[FEDERATION_BACKEND_METHODS.getNavigationLaunchpadConfig]: "thread_detail",
|
|
19947
|
+
[FEDERATION_BACKEND_METHODS.getNavigationSelectedDetail]: "thread_detail",
|
|
19948
|
+
[FEDERATION_BACKEND_METHODS.getNavigationQueueProjection]: "thread_detail",
|
|
19949
|
+
[FEDERATION_BACKEND_METHODS.getProjectPage]: "thread_navigation",
|
|
19950
|
+
[FEDERATION_BACKEND_METHODS.getNavigationDescendantPage]: "thread_navigation",
|
|
19951
|
+
[FEDERATION_BACKEND_METHODS.lookupArchivedThreads]: "thread_navigation",
|
|
19952
|
+
[FEDERATION_BACKEND_METHODS.getNavigationSnapshot]: "thread_navigation",
|
|
19953
|
+
[FEDERATION_BACKEND_METHODS.searchNavigationThreads]: "thread_navigation",
|
|
19954
|
+
[FEDERATION_BACKEND_METHODS.searchFederatedThreads]: "federated_search",
|
|
19955
|
+
[FEDERATION_BACKEND_METHODS.searchThreadContent]: "thread_detail",
|
|
19956
|
+
[FEDERATION_BACKEND_METHODS.cancelThreadContentSearch]: "thread_detail",
|
|
19957
|
+
[FEDERATION_BACKEND_METHODS.listThreads]: "thread_navigation",
|
|
19958
|
+
[FEDERATION_BACKEND_METHODS.resolveThread]: "thread_navigation",
|
|
19959
|
+
[FEDERATION_BACKEND_METHODS.resolveThreadAdmissionState]: "messaging_route",
|
|
19960
|
+
[FEDERATION_BACKEND_METHODS.readThread]: "thread_detail",
|
|
19961
|
+
[FEDERATION_BACKEND_METHODS.readTurnAssistantReply]: "thread_detail",
|
|
19962
|
+
[FEDERATION_BACKEND_METHODS.readForge]: "thread_detail",
|
|
19963
|
+
/* Reads the thread's own transcript history; same data class as reading it. */
|
|
19964
|
+
[FEDERATION_BACKEND_METHODS.readUsageActivity]: "thread_detail",
|
|
19965
|
+
[FEDERATION_BACKEND_METHODS.readThreadFamilyPricing]: "thread_detail",
|
|
19966
|
+
// Starts a paid helper turn on the owner; transcript-read permission is insufficient.
|
|
19967
|
+
[FEDERATION_BACKEND_METHODS.analyzeUsageActivity]: "turn_control",
|
|
19968
|
+
[FEDERATION_BACKEND_METHODS.inspectTokenMiserOutput]: "thread_detail",
|
|
19969
|
+
[FEDERATION_BACKEND_METHODS.analyzeThreadToolHistory]: "thread_detail",
|
|
19970
|
+
[FEDERATION_BACKEND_METHODS.readTranscriptImage]: "thread_detail",
|
|
19971
|
+
[FEDERATION_BACKEND_METHODS.listSkills]: "thread_detail",
|
|
19972
|
+
[FEDERATION_BACKEND_METHODS.supportsMessagingPdfTools]: "messaging_route",
|
|
19973
|
+
[FEDERATION_BACKEND_METHODS.listBackends]: "thread_detail",
|
|
19974
|
+
[FEDERATION_BACKEND_METHODS.markThreadSeen]: "thread_navigation",
|
|
19975
|
+
[FEDERATION_BACKEND_METHODS.continueThreadEnvironmentStartup]: "turn_control",
|
|
19976
|
+
[FEDERATION_BACKEND_METHODS.acknowledgeThreadEnvironmentFailure]: "thread_navigation",
|
|
19977
|
+
[FEDERATION_BACKEND_METHODS.setThreadReaction]: "thread_navigation",
|
|
19978
|
+
// A lock refuses every turn on the owner's thread, so it takes the same
|
|
19979
|
+
// capability as starting one.
|
|
19980
|
+
[FEDERATION_BACKEND_METHODS.setThreadLock]: "turn_control",
|
|
19981
|
+
[FEDERATION_BACKEND_METHODS.setThreadPin]: "thread_navigation",
|
|
19982
|
+
[FEDERATION_BACKEND_METHODS.reorderThreadPins]: "thread_navigation",
|
|
19983
|
+
[FEDERATION_BACKEND_METHODS.mountRemoteChild]: "thread_navigation",
|
|
19984
|
+
[FEDERATION_BACKEND_METHODS.setThreadParent]: "thread_grouping",
|
|
19985
|
+
[FEDERATION_BACKEND_METHODS.mutateThreadParentV1]: "thread_grouping",
|
|
19986
|
+
[FEDERATION_BACKEND_METHODS.updateSubthreadOrder]: "thread_grouping",
|
|
19987
|
+
[FEDERATION_BACKEND_METHODS.setSubthreadsCollapsed]: "thread_grouping",
|
|
19988
|
+
// PR detach cancels pending auto-dispatch work and auto-dispatch arms
|
|
19989
|
+
// automatic repair turns, so both sit with the turn-control grants
|
|
19990
|
+
// (like archiveThread) rather than the browse-level navigation grants.
|
|
19991
|
+
[FEDERATION_BACKEND_METHODS.detachThreadPullRequest]: "turn_control",
|
|
19992
|
+
[FEDERATION_BACKEND_METHODS.setThreadPrAutoDispatch]: "turn_control",
|
|
19993
|
+
[FEDERATION_BACKEND_METHODS.cancelThreadPrAutoDispatch]: "turn_control",
|
|
19994
|
+
[FEDERATION_BACKEND_METHODS.sendThreadPrAutoDispatchNow]: "turn_control",
|
|
19995
|
+
// A watch wakes the thread with an agent turn, and stopping one cancels
|
|
19996
|
+
// that wake, so both sit with Auto-fix under turn control.
|
|
19997
|
+
[FEDERATION_BACKEND_METHODS.manageThreadPrWatchesV1]: "turn_control",
|
|
19998
|
+
// A project's Forge Automation decides which turns the owner starts on its
|
|
19999
|
+
// own, and a held wake-up's Send starts one, so all three take the
|
|
20000
|
+
// turn-control grant. Listing rides with them: only the resume flow asks.
|
|
20001
|
+
[FEDERATION_BACKEND_METHODS.setProjectForgeAutomation]: "turn_control",
|
|
20002
|
+
[FEDERATION_BACKEND_METHODS.listProjectHeldWakeups]: "turn_control",
|
|
20003
|
+
[FEDERATION_BACKEND_METHODS.resolveProjectHeldWakeups]: "turn_control",
|
|
20004
|
+
[FEDERATION_BACKEND_METHODS.archiveThread]: "turn_control",
|
|
20005
|
+
[FEDERATION_BACKEND_METHODS.restoreThread]: "turn_control",
|
|
20006
|
+
[FEDERATION_BACKEND_METHODS.startThread]: "turn_control",
|
|
20007
|
+
[FEDERATION_BACKEND_METHODS.forkThread]: "turn_control",
|
|
20008
|
+
[FEDERATION_BACKEND_METHODS.startTurn]: "turn_control",
|
|
20009
|
+
[FEDERATION_BACKEND_METHODS.submitTurnRequest]: "turn_control",
|
|
20010
|
+
[FEDERATION_BACKEND_METHODS.getTurnRequest]: "turn_control",
|
|
20011
|
+
[FEDERATION_BACKEND_METHODS.submitSteerRequest]: "turn_control",
|
|
20012
|
+
[FEDERATION_BACKEND_METHODS.getSteerRequest]: "turn_control",
|
|
20013
|
+
[FEDERATION_BACKEND_METHODS.replaceQueuedMessage]: "turn_control",
|
|
20014
|
+
[FEDERATION_BACKEND_METHODS.readQueuedTurn]: "thread_detail",
|
|
20015
|
+
[FEDERATION_BACKEND_METHODS.cancelQueuedTurn]: "turn_control",
|
|
20016
|
+
[FEDERATION_BACKEND_METHODS.releaseQueuedTurn]: "turn_control",
|
|
20017
|
+
[FEDERATION_BACKEND_METHODS.moveQueuedTurn]: "turn_control",
|
|
20018
|
+
[FEDERATION_BACKEND_METHODS.setQueuedTurnPaused]: "turn_control",
|
|
20019
|
+
[FEDERATION_BACKEND_METHODS.startReview]: "turn_control",
|
|
20020
|
+
[FEDERATION_BACKEND_METHODS.startPullRequestReview]: "turn_control",
|
|
20021
|
+
[FEDERATION_BACKEND_METHODS.listScheduledThreadActions]: "scheduled_actions",
|
|
20022
|
+
[FEDERATION_BACKEND_METHODS.createScheduledThreadAction]: "scheduled_actions",
|
|
20023
|
+
[FEDERATION_BACKEND_METHODS.createScheduledPullRequestReview]: "scheduled_actions",
|
|
20024
|
+
[FEDERATION_BACKEND_METHODS.updateScheduledThreadAction]: "scheduled_actions",
|
|
20025
|
+
[FEDERATION_BACKEND_METHODS.updateScheduledPullRequestReview]: "scheduled_actions",
|
|
20026
|
+
[FEDERATION_BACKEND_METHODS.cancelScheduledThreadAction]: "scheduled_actions",
|
|
20027
|
+
[FEDERATION_BACKEND_METHODS.sendScheduledThreadActionNow]: "scheduled_actions",
|
|
20028
|
+
[FEDERATION_BACKEND_METHODS.compactThread]: "turn_control",
|
|
20029
|
+
[FEDERATION_BACKEND_METHODS.rewindThread]: "turn_control",
|
|
20030
|
+
[FEDERATION_BACKEND_METHODS.previewThreadRewind]: "turn_control",
|
|
20031
|
+
[FEDERATION_BACKEND_METHODS.undoThreadRewind]: "turn_control",
|
|
20032
|
+
[FEDERATION_BACKEND_METHODS.listMcpConnections]: "thread_detail",
|
|
20033
|
+
[FEDERATION_BACKEND_METHODS.readThreadMcpConnections]: "thread_detail",
|
|
20034
|
+
[FEDERATION_BACKEND_METHODS.setThreadMcpConnections]: "turn_control",
|
|
20035
|
+
[FEDERATION_BACKEND_METHODS.describeThreadMcpConnections]: "thread_detail",
|
|
20036
|
+
[FEDERATION_BACKEND_METHODS.listThreadMcpServers]: "thread_detail",
|
|
20037
|
+
[FEDERATION_BACKEND_METHODS.reloadCodexMcpConfig]: "turn_control",
|
|
20038
|
+
[FEDERATION_BACKEND_METHODS.controlActiveTurn]: "turn_control",
|
|
20039
|
+
[FEDERATION_BACKEND_METHODS.resolveActiveTurn]: "turn_control",
|
|
20040
|
+
[FEDERATION_BACKEND_METHODS.interruptTurn]: "turn_control",
|
|
20041
|
+
[FEDERATION_BACKEND_METHODS.stopSubAgent]: "turn_control",
|
|
20042
|
+
[FEDERATION_BACKEND_METHODS.steerTurn]: "turn_control",
|
|
20043
|
+
[FEDERATION_BACKEND_METHODS.setThreadExecutionMode]: "turn_control",
|
|
20044
|
+
[FEDERATION_BACKEND_METHODS.queueThreadExecutionMode]: "turn_control",
|
|
20045
|
+
[FEDERATION_BACKEND_METHODS.cancelThreadExecutionModeQueue]: "turn_control",
|
|
20046
|
+
[FEDERATION_BACKEND_METHODS.setAcpSessionRuntimeOption]: "turn_control",
|
|
20047
|
+
[FEDERATION_BACKEND_METHODS.setThreadAgent]: "turn_control",
|
|
20048
|
+
[FEDERATION_BACKEND_METHODS.setThreadTokenMiser]: "turn_control",
|
|
20049
|
+
[FEDERATION_BACKEND_METHODS.setThreadModelSettings]: "turn_control",
|
|
20050
|
+
[FEDERATION_BACKEND_METHODS.checkThreadBranchDrift]: "thread_navigation",
|
|
20051
|
+
[FEDERATION_BACKEND_METHODS.updateThreadExpectedBranch]: "turn_control",
|
|
20052
|
+
[FEDERATION_BACKEND_METHODS.retainThreadBranchDrift]: "turn_control",
|
|
20053
|
+
[FEDERATION_BACKEND_METHODS.submitServerRequest]: "pending_request_control",
|
|
20054
|
+
[FEDERATION_BACKEND_METHODS.runCodexEnvironmentAction]: "environment_actions",
|
|
20055
|
+
[FEDERATION_BACKEND_METHODS.stopCodexEnvironmentAction]: "environment_actions",
|
|
20056
|
+
[FEDERATION_BACKEND_METHODS.listBackgroundTerminals]: "thread_detail",
|
|
20057
|
+
[FEDERATION_BACKEND_METHODS.terminateBackgroundTerminal]: "turn_control",
|
|
20058
|
+
[FEDERATION_BACKEND_METHODS.setCodexThreadEnvironment]: "environment_actions",
|
|
20059
|
+
[FEDERATION_BACKEND_METHODS.refreshThreadPullRequests]: "thread_navigation",
|
|
20060
|
+
[FEDERATION_BACKEND_METHODS.refreshDirectoryGitStatuses]: "thread_navigation",
|
|
20061
|
+
[FEDERATION_BACKEND_METHODS.refreshThreadGitWorkingState]: "thread_navigation",
|
|
20062
|
+
[FEDERATION_BACKEND_METHODS.ensureDirectoryLaunchpad]: "launchpad_metadata",
|
|
20063
|
+
[FEDERATION_BACKEND_METHODS.listRecentFileReferences]: "remote_window",
|
|
20064
|
+
[FEDERATION_BACKEND_METHODS.recordRecentFileReferences]: "remote_window",
|
|
20065
|
+
// Reviewer/composer model-settings history is read-only preference data at
|
|
20066
|
+
// the same sensitivity tier as the recent-file list it sits beside.
|
|
20067
|
+
[FEDERATION_BACKEND_METHODS.listModelSettingsRecents]: "remote_window",
|
|
20068
|
+
[FEDERATION_BACKEND_METHODS.recordModelSettingsRecent]: "remote_window",
|
|
20069
|
+
[FEDERATION_BACKEND_METHODS.attachDirectoryToThread]: "environment_actions",
|
|
20070
|
+
[FEDERATION_BACKEND_METHODS.detachDirectoryFromThread]: "environment_actions",
|
|
20071
|
+
[FEDERATION_BACKEND_METHODS.listWorktreeUnpublishedCommits]: "thread_detail",
|
|
20072
|
+
[FEDERATION_BACKEND_METHODS.getWorktreeUnpublishedCommitDiff]: "thread_detail",
|
|
20073
|
+
[FEDERATION_BACKEND_METHODS.materializeDirectoryLaunchpad]: "environment_actions",
|
|
20074
|
+
[FEDERATION_BACKEND_METHODS.manageLaunchpadCreation]: "environment_actions",
|
|
20075
|
+
[FEDERATION_BACKEND_METHODS.handoffThreadWorkspace]: "turn_control",
|
|
20076
|
+
[FEDERATION_BACKEND_METHODS.handoffThreadWorkspaceToNewWorktree]: "turn_control",
|
|
20077
|
+
[FEDERATION_BACKEND_METHODS.recreateThreadWorktree]: "turn_control",
|
|
20078
|
+
[FEDERATION_BACKEND_METHODS.moveThreadToMainCheckout]: "turn_control",
|
|
20079
|
+
[FEDERATION_BACKEND_METHODS.renameThread]: "turn_control",
|
|
20080
|
+
[FEDERATION_BACKEND_METHODS.readApplications]: "remote_window",
|
|
20081
|
+
[FEDERATION_BACKEND_METHODS.openApplication]: "remote_window",
|
|
20082
|
+
// Read-only peer messaging health for the remote window's MSG chip.
|
|
20083
|
+
// messaging_route stays reserved for messaging-originated remote control.
|
|
20084
|
+
[FEDERATION_BACKEND_METHODS.readMessagingPlatformStatuses]: "remote_window",
|
|
20085
|
+
[FEDERATION_BACKEND_METHODS.readPwrSnapConnectionStatus]: "pwrsnap_connection",
|
|
20086
|
+
// On-demand load readings (CPU load averages, available RAM, free
|
|
20087
|
+
// disk) are read-only health facts at the same sensitivity tier as
|
|
20088
|
+
// the browse-level grants, and the Star Map surface that polls them
|
|
20089
|
+
// already rides thread_navigation for its event class. A dedicated
|
|
20090
|
+
// capability would buy no isolation and cost every peer a handshake
|
|
20091
|
+
// re-advertisement.
|
|
20092
|
+
[FEDERATION_BACKEND_METHODS.getLoadStatus]: "thread_navigation",
|
|
20093
|
+
[FEDERATION_BACKEND_METHODS.trustCodexProject]: "environment_actions",
|
|
20094
|
+
// Celestial icon overrides are directed at the gateway (the assignment
|
|
20095
|
+
// coordinator). No dedicated capability exists for federation-level
|
|
20096
|
+
// cosmetic state; thread_navigation is the least-privileged grant every
|
|
20097
|
+
// browsing peer already holds.
|
|
20098
|
+
[FEDERATION_BACKEND_METHODS.setCelestialIcon]: "thread_navigation",
|
|
20099
|
+
// Short-name overrides go to the gateway for the same reason, under the
|
|
20100
|
+
// same least-privileged grant.
|
|
20101
|
+
[FEDERATION_BACKEND_METHODS.setFederationShortName]: "thread_navigation",
|
|
20102
|
+
// Intake creates a thread and starts its first turn on the owning
|
|
20103
|
+
// instance — the same trust the materialize-launchpad path carries.
|
|
20104
|
+
[FEDERATION_BACKEND_METHODS.starMapIntake]: "environment_actions"
|
|
20105
|
+
};
|
|
19489
20106
|
export {
|
|
19490
20107
|
AgentToolRouter,
|
|
20108
|
+
FEDERATION_BACKEND_METHOD_CAPABILITIES,
|
|
19491
20109
|
FEDERATION_CLOSE_REPLACED_CODE,
|
|
19492
20110
|
FEDERATION_CLOSE_REVOKED_CODE,
|
|
19493
20111
|
FEDERATION_INVITE_VERSION,
|
|
@@ -19547,6 +20165,7 @@ export {
|
|
|
19547
20165
|
decodeFederationInvite,
|
|
19548
20166
|
encodeFederationInvite,
|
|
19549
20167
|
federationTransportCodecForTest,
|
|
20168
|
+
fitThreadReplayWithinByteBudget,
|
|
19550
20169
|
generateFederationIdentityKeyPair,
|
|
19551
20170
|
generateFederationNoiseStaticKeyPair,
|
|
19552
20171
|
generateNoiseStaticKeyPair,
|
|
@@ -19558,6 +20177,7 @@ export {
|
|
|
19558
20177
|
mergeCausalMessagingContext,
|
|
19559
20178
|
noiseKeyPairFromRawPrivate,
|
|
19560
20179
|
noisePublicKeyBase64FromPrivateBase64,
|
|
20180
|
+
pageThreadReplay,
|
|
19561
20181
|
publicKeyPemFromPrivateKey,
|
|
19562
20182
|
pushFederationFile,
|
|
19563
20183
|
rawPrivateKey,
|
|
@@ -19566,6 +20186,7 @@ export {
|
|
|
19566
20186
|
registerFederationPtyHandlers,
|
|
19567
20187
|
registerFilePushHandlers,
|
|
19568
20188
|
resolveMessagingOutboundFile,
|
|
20189
|
+
shortenDerivedThreadTitle,
|
|
19569
20190
|
signFederationMessage,
|
|
19570
20191
|
validateMessagingFileMetadata,
|
|
19571
20192
|
verifyFederationMessageSignature,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pwragent",
|
|
3
|
-
"version": "1.3.
|
|
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.
|
|
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.
|
|
67
|
-
"commit": "
|
|
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
|
|
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,16 +1,32 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { mergeCausalMessagingContext } from "../dist/federation-core.js";
|
|
4
|
+
import { FEDERATION_BACKEND_METHOD_CAPABILITIES, FEDERATION_MAX_FRAME_BYTES, fitThreadReplayWithinByteBudget, mergeCausalMessagingContext, pageThreadReplay, shortenDerivedThreadTitle } from "../dist/federation-core.js";
|
|
5
5
|
import { ACTIVE, atomicJson, digest } from "./store.js";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
// The owner methods this outpost serves. Each one's capability comes from the
|
|
8
|
+
// shared owner contract, so a grant means the same here as on desktop.
|
|
9
|
+
const OWNER_METHODS = [
|
|
10
|
+
"resolveThreadAdmissionState", "supportsMessagingPdfTools",
|
|
11
|
+
"getNavigationQueryPage", "getNavigationLaunchpadConfig", "getNavigationSelectedDetail", "getNavigationQueueProjection",
|
|
12
|
+
"releaseNavigationAttentionView", "listThreads", "listBackends", "resolveThread", "markThreadSeen", "getProjectPage", "lookupArchivedThreads",
|
|
13
|
+
"readThread", "readTurnAssistantReply", "listSkills", "listThreadMcpServers",
|
|
14
|
+
"startThread", "materializeDirectoryLaunchpad", "startTurn", "interruptTurn", "steerTurn", "controlActiveTurn", "renameThread",
|
|
15
|
+
"setThreadModelSettings", "setThreadExecutionMode",
|
|
16
|
+
];
|
|
17
|
+
export function ownerMethodCapabilities(methods, contract = FEDERATION_BACKEND_METHOD_CAPABILITIES) {
|
|
18
|
+
return Object.fromEntries(methods.map((name) => {
|
|
19
|
+
const method = `backend.${name}`;
|
|
20
|
+
if (!Object.hasOwn(contract, method)) throw new Error(`${method} is not in the shared owner contract`);
|
|
21
|
+
return [method, contract[method]];
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export const METHOD_CAPABILITIES = ownerMethodCapabilities(OWNER_METHODS);
|
|
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";
|
|
14
30
|
|
|
15
31
|
function mode(sandbox) { return sandbox === "workspace-write" ? "auto" : "default"; }
|
|
16
32
|
function sandboxFor(params, fallback = "read-only") {
|
|
@@ -33,12 +49,16 @@ export class HeadlessBackend {
|
|
|
33
49
|
this.runtime = runtime;
|
|
34
50
|
this.publish = publish;
|
|
35
51
|
this.epoch = randomUUID();
|
|
52
|
+
// Same headroom as desktop's FEDERATION_RESPONSE_BYTE_BUDGET.
|
|
53
|
+
this.replayByteBudget = FEDERATION_MAX_FRAME_BYTES - 64 * 1024;
|
|
36
54
|
this.attentionViews = new Map();
|
|
37
55
|
this.file = path.join(path.dirname(runtime.store.directory), "threads.json");
|
|
38
56
|
try {
|
|
39
57
|
const data = JSON.parse(readFileSync(this.file, "utf8"));
|
|
40
58
|
if (data.version !== 1 || !Array.isArray(data.threads)) throw new Error("Invalid headless thread registry");
|
|
41
|
-
this.threads = new Map(data.threads.map((thread) => [thread.id,
|
|
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]));
|
|
42
62
|
} catch (error) {
|
|
43
63
|
if (error.code !== "ENOENT") throw error;
|
|
44
64
|
this.threads = new Map();
|
|
@@ -94,6 +114,10 @@ export class HeadlessBackend {
|
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
event(message) {
|
|
117
|
+
if (message.method === "headless/turnStarted") {
|
|
118
|
+
this.titleFromFirstPrompt(message.params);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
97
121
|
if (message.method === "headless/jobChanged") {
|
|
98
122
|
this.syncJobs([message.params]);
|
|
99
123
|
if (message.params.threadId) this.forgetSeenAttentionMember(message.params.threadId);
|
|
@@ -149,7 +173,14 @@ export class HeadlessBackend {
|
|
|
149
173
|
const messages = history.replay.messages.filter((message) => message.role === "assistant" && message.turn?.id === params.turnId);
|
|
150
174
|
return { message: messages.find((message) => params.itemId ? message.id === params.itemId : message.text === params.text) || null };
|
|
151
175
|
}
|
|
152
|
-
case "readThread":
|
|
176
|
+
case "readThread": {
|
|
177
|
+
// Viewers take the newest page as returned. Page and fit it to one
|
|
178
|
+
// frame here, as desktop owners do.
|
|
179
|
+
const history = await this.readThread(params.threadId);
|
|
180
|
+
return { ...history, replay: fitThreadReplayWithinByteBudget({ cursorIdSpace: "entry-id",
|
|
181
|
+
replay: pageThreadReplay(history.replay, params), maxBytes: this.replayByteBudget,
|
|
182
|
+
measureBytes: (replay) => Buffer.byteLength(JSON.stringify({ ...history, replay }), "utf8") }) };
|
|
183
|
+
}
|
|
153
184
|
case "supportsMessagingPdfTools": return params.backend === "codex" && this.owned(params.threadId).messagingFileTools === true;
|
|
154
185
|
case "startThread": return this.startThread(params);
|
|
155
186
|
case "materializeDirectoryLaunchpad": return this.materializeLaunchpad(params, envelope);
|
|
@@ -186,9 +217,8 @@ export class HeadlessBackend {
|
|
|
186
217
|
const thread = this.owned(params.threadId);
|
|
187
218
|
const title = params.title || params.name;
|
|
188
219
|
if (typeof title !== "string" || !title.trim() || title.length > 200) throw new Error("Invalid title");
|
|
189
|
-
|
|
190
|
-
this.
|
|
191
|
-
this.invalidate(thread.id);
|
|
220
|
+
thread.updatedAt = Date.now();
|
|
221
|
+
this.setTitle(thread, title.trim(), "explicit");
|
|
192
222
|
return { backend: "codex", threadId: thread.id, title: thread.title };
|
|
193
223
|
}
|
|
194
224
|
case "markThreadSeen": {
|
|
@@ -196,13 +226,18 @@ export class HeadlessBackend {
|
|
|
196
226
|
for (const key of ["seenAt", "seenUpdatedAt"]) {
|
|
197
227
|
if (params[key] !== undefined && (!Number.isFinite(params[key]) || params[key] < 0)) throw new Error(`Invalid ${key}`);
|
|
198
228
|
}
|
|
199
|
-
|
|
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;
|
|
200
233
|
thread.seenAt = params.seenAt ?? Date.now();
|
|
201
|
-
thread.seenUpdatedAt = seenUpdatedAt;
|
|
202
234
|
this.persist();
|
|
203
235
|
this.forgetSeenAttentionMember(thread.id);
|
|
204
|
-
this.
|
|
205
|
-
|
|
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 } : {}) };
|
|
206
241
|
}
|
|
207
242
|
case "setThreadExecutionMode": {
|
|
208
243
|
const thread = this.owned(params.threadId);
|
|
@@ -226,7 +261,35 @@ export class HeadlessBackend {
|
|
|
226
261
|
}
|
|
227
262
|
}
|
|
228
263
|
|
|
229
|
-
invalidate(threadId
|
|
264
|
+
invalidate(threadId, sourceMethod) {
|
|
265
|
+
this.publish({ backend: "codex", notification: { method: "navigation/invalidated", params: { threadId, ...(sourceMethod ? { sourceMethod } : {}) } } }, "navigation");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// threads.json is the only store for a title. The outpost never sets the
|
|
269
|
+
// Codex thread name.
|
|
270
|
+
setTitle(thread, title, titleSource) {
|
|
271
|
+
Object.assign(thread, { title, titleSource });
|
|
272
|
+
this.persist();
|
|
273
|
+
this.publish({ backend: "codex", notification: { method: "thread/name/updated",
|
|
274
|
+
params: { threadId: thread.id, threadName: title, titleSource } } }, "transcript");
|
|
275
|
+
this.invalidate(thread.id, "thread/name/updated");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// The outpost has no helper model, so a thread is named as desktop first
|
|
279
|
+
// names one: the shortened first user message. Any rename wins over it.
|
|
280
|
+
deriveTitle(id, text) {
|
|
281
|
+
const thread = this.threads.get(id);
|
|
282
|
+
if (!thread || thread.archived || thread.titleSource !== "fallback" || typeof text !== "string") return;
|
|
283
|
+
const title = shortenDerivedThreadTitle(text)?.slice(0, 200);
|
|
284
|
+
if (title) this.setTitle(thread, title, "derived");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
titleFromFirstPrompt({ threadId, jobId, prompt }) {
|
|
288
|
+
// A later turn is not the first message. readThread names that thread
|
|
289
|
+
// from Codex's preview instead.
|
|
290
|
+
const earlier = [...this.runtime.store.jobs.values()].some((job) => job.threadId === threadId && job.id !== jobId && job.turnId);
|
|
291
|
+
if (!earlier) this.deriveTitle(threadId, prompt);
|
|
292
|
+
}
|
|
230
293
|
|
|
231
294
|
async materializeLaunchpad(params, envelope) {
|
|
232
295
|
const cwd = this.runtime.projects.resolve(params.directoryKey);
|
|
@@ -264,11 +327,11 @@ export class HeadlessBackend {
|
|
|
264
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 });
|
|
265
328
|
if (!result?.thread?.id || typeof result.thread.id !== "string" || result.thread.id.length > 256) throw new Error("Invalid thread/start response");
|
|
266
329
|
this.runtime.loadedThreads.add(result.thread.id);
|
|
267
|
-
const thread = { id: result.thread.id, messagingFileTools: true, title:
|
|
330
|
+
const thread = { id: result.thread.id, messagingFileTools: true, title: PLACEHOLDER_THREAD_TITLE, titleSource: "fallback", createdAt: Date.now(), cwd, sandbox,
|
|
268
331
|
model: params.model, reasoningEffort: params.reasoningEffort, serviceTier: params.serviceTier };
|
|
269
332
|
this.threads.set(thread.id, thread);
|
|
270
333
|
this.persist();
|
|
271
|
-
this.invalidate(thread.id);
|
|
334
|
+
this.invalidate(thread.id, "thread/started");
|
|
272
335
|
return { backend: "codex", threadId: thread.id, executionMode: mode(sandbox) };
|
|
273
336
|
} finally { this.creating = false; }
|
|
274
337
|
}
|
|
@@ -328,6 +391,9 @@ export class HeadlessBackend {
|
|
|
328
391
|
}
|
|
329
392
|
}
|
|
330
393
|
const messages = entries.filter((entry) => entry.type === "message");
|
|
394
|
+
// Names threads created before derived titles, and imported threads.
|
|
395
|
+
this.deriveTitle(id, typeof result.thread?.preview === "string" && result.thread.preview.trim()
|
|
396
|
+
? result.thread.preview : messages.find((item) => item.role === "user")?.text);
|
|
331
397
|
return { backend: "codex", threadId: id, fetchedAt: Date.now(), threadStatus: this.summary(this.owned(id)).threadStatus,
|
|
332
398
|
replay: { entries, messages, lastUserMessage: messages.filter((item) => item.role === "user").at(-1)?.text,
|
|
333
399
|
lastAssistantMessage: messages.filter((item) => item.role === "assistant").at(-1)?.text,
|
package/src/federation-core.ts
CHANGED
|
@@ -16,9 +16,10 @@ export * from "@pwragent/federation/git-instance-handoff";
|
|
|
16
16
|
export * from "@pwragent/federation/federation-file-push";
|
|
17
17
|
|
|
18
18
|
export { AgentToolRouter } from "@pwragent/agent-tools/agent-tool-router";
|
|
19
|
-
export { mergeCausalMessagingContext } from "@pwragent/shared";
|
|
19
|
+
export { fitThreadReplayWithinByteBudget, mergeCausalMessagingContext, pageThreadReplay, shortenDerivedThreadTitle } from "@pwragent/shared";
|
|
20
20
|
export { authenticateMessageOrigin } from "@pwragent/federation/federation-message-origin";
|
|
21
21
|
|
|
22
22
|
export { resolveMessagingOutboundFile, validateMessagingFileMetadata } from "@pwragent/agent-tools/messaging-outbound-file";
|
|
23
23
|
|
|
24
24
|
export * from "@pwragent/federation/federation-replacement-receiver";
|
|
25
|
+
export { FEDERATION_BACKEND_METHOD_CAPABILITIES } from "@pwragent/federation/federation-backend-methods";
|
package/src/handoffs.js
CHANGED
|
@@ -77,7 +77,9 @@ export function setupHandoffs(federation, directory) {
|
|
|
77
77
|
const file = await runtime.codex.request("fs/readFile", { path: result.thread.path });
|
|
78
78
|
if (typeof file.dataBase64 !== "string") throw new Error("Codex protocol thread export is unavailable");
|
|
79
79
|
return { rolloutBase64: file.dataBase64, replay: (await backend.readThread(id)).replay,
|
|
80
|
-
cwd: runtime.projects.resolve(owned.cwd || runtime.workspace),
|
|
80
|
+
cwd: runtime.projects.resolve(owned.cwd || runtime.workspace),
|
|
81
|
+
// A placeholder would become the destination's explicit name.
|
|
82
|
+
...(owned.titleSource === "fallback" ? {} : { title: owned.title }) };
|
|
81
83
|
},
|
|
82
84
|
forkThread: async (request) => {
|
|
83
85
|
if (runtime.active || backend.creating) throw new Error("Outpost is busy");
|
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);
|
|
@@ -151,6 +155,8 @@ export class Runtime {
|
|
|
151
155
|
return;
|
|
152
156
|
}
|
|
153
157
|
this.save({ ...this.store.get(job.id), turnId: turn.turn.id, status: "running" });
|
|
158
|
+
// The prompt stays in memory: job records never store it.
|
|
159
|
+
this.onEvent?.({ method: "headless/turnStarted", params: { threadId: result.thread.id, jobId: job.id, prompt } });
|
|
154
160
|
if (this.earlyCompletion?.turn?.id === turn.turn.id) {
|
|
155
161
|
this.complete(this.earlyCompletion);
|
|
156
162
|
} else if (["completed", "failed", "interrupted"].includes(turn.turn.status)) {
|
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
|
+
}
|