pwragent 1.3.8 → 1.3.9
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 +28 -6
- package/dist/federation-core.js +645 -24
- package/package.json +3 -3
- package/src/federation-backend.js +67 -13
- package/src/federation-core.ts +2 -1
- package/src/handoffs.js +3 -1
- package/src/runtime.js +2 -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
|
|
@@ -70,7 +70,7 @@ shell's Node version-manager initialization does not run under systemd.
|
|
|
70
70
|
After publication, install a pinned version into the service prefix:
|
|
71
71
|
|
|
72
72
|
```sh
|
|
73
|
-
sudo npm install --prefix /opt/pwragent --omit=dev --ignore-scripts pwragent@1.3.
|
|
73
|
+
sudo npm install --prefix /opt/pwragent --omit=dev --ignore-scripts pwragent@1.3.7
|
|
74
74
|
sudo install -m 0644 /opt/pwragent/node_modules/pwragent/examples/pwragent-headless.service /etc/systemd/system/pwragent-headless.service
|
|
75
75
|
# Review the account, paths, profile and Codex configuration before enabling:
|
|
76
76
|
sudo systemctl daemon-reload
|
|
@@ -90,7 +90,7 @@ sudo -u pwragent env PWRAGENT_HOME=/var/lib/pwragent PWRAGENT_PROFILE=outpost \
|
|
|
90
90
|
|
|
91
91
|
For a service update, finish or interrupt active work, run
|
|
92
92
|
`sudo systemctl stop pwragent-headless.service`, repeat the prefix installation
|
|
93
|
-
with the chosen new version (or `pwragent@
|
|
93
|
+
with the chosen new version (or `pwragent@latest`), then run
|
|
94
94
|
`sudo systemctl start pwragent-headless.service`. Compare the new example unit
|
|
95
95
|
with the installed one before deliberately updating it; npm does not overwrite
|
|
96
96
|
`/etc/systemd/system`. Stopping during a turn can leave its outcome `unknown`.
|
|
@@ -280,6 +280,15 @@ its `directoryKey`/`cwd`; omitting it selects the configured workspace root. Sel
|
|
|
280
280
|
the defaults remain local and read-only. Launchpad requests for worktrees,
|
|
281
281
|
grouping, environments or provider extensions are rejected before creation.
|
|
282
282
|
|
|
283
|
+
A new thread is named from its first user message, shortened the way desktop
|
|
284
|
+
names a thread before any helper-model title. The outpost has no helper model.
|
|
285
|
+
The name is stored in `threads.json`, and subscribed viewers receive
|
|
286
|
+
`thread/name/updated` with `titleSource: derived`, followed by a navigation
|
|
287
|
+
invalidation. A later turn never renames a thread. An explicit `renameThread`
|
|
288
|
+
always wins and is never replaced. A thread that still has a placeholder name,
|
|
289
|
+
such as one created before this release, takes Codex's `preview` the next time
|
|
290
|
+
it is read. The outpost does not set the Codex thread name.
|
|
291
|
+
|
|
283
292
|
## Identity, compute lifetime and remaining scope
|
|
284
293
|
|
|
285
294
|
The private `federation.json` stores the retained peer ID, signing/Noise keys,
|
|
@@ -349,6 +358,7 @@ register a project. Enrollment and Git intake do not import the sender's Git or
|
|
|
349
358
|
|
|
350
359
|
| Service | Required grants | Supported behavior |
|
|
351
360
|
| --- | --- | --- |
|
|
361
|
+
| `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
362
|
| `outpost.projects.list` | `thread_navigation` | Owner project inventory |
|
|
353
363
|
| `outpost.projects.create/add/remove` | `environment_actions` | Create/clone, register and unregister projects |
|
|
354
364
|
| `pty.*` | `remote_pty` | Input, resize, ack flow control, snapshots, history and close |
|
|
@@ -465,6 +475,18 @@ against the daemon. External providers and Codex are mocked; no live peers,
|
|
|
465
475
|
operator projects, credentials or compute machines are used. Ubuntu CI runs the
|
|
466
476
|
Node suite; the desktop contract test runs on POSIX lanes.
|
|
467
477
|
|
|
478
|
+
The owner conformance suite
|
|
479
|
+
(`apps/desktop/src/main/__tests__/owner-conformance.test.ts`) runs one thread's
|
|
480
|
+
life through the owner methods both hosts implement. It runs on this daemon and
|
|
481
|
+
on desktop's real owner, and compares each response and event. Each known
|
|
482
|
+
difference is a line in `owner-conformance-baseline.json` beside it, so the
|
|
483
|
+
suite fails on any new difference and names its method. After fixing a
|
|
484
|
+
difference, or changing the contract on purpose, record the result:
|
|
485
|
+
|
|
486
|
+
```sh
|
|
487
|
+
UPDATE_OWNER_CONFORMANCE=1 pnpm test apps/desktop/src/main/__tests__/owner-conformance.test.ts
|
|
488
|
+
```
|
|
489
|
+
|
|
468
490
|
A separate live smoke test installed the packed tarball and official Codex
|
|
469
491
|
0.159.2 in a Node 24 Debian Linux arm64 Docker image. It used an unprivileged
|
|
470
492
|
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.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Experimental headless PwrAgent daemon and command-line client.",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"node-pty"
|
|
64
64
|
],
|
|
65
65
|
"pwragentRelease": {
|
|
66
|
-
"tag": "v1.3.
|
|
67
|
-
"commit": "
|
|
66
|
+
"tag": "v1.3.9",
|
|
67
|
+
"commit": "0fc28acd4318e55e172427b5a694c1a1dd096c15"
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -1,16 +1,27 @@
|
|
|
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);
|
|
14
25
|
|
|
15
26
|
function mode(sandbox) { return sandbox === "workspace-write" ? "auto" : "default"; }
|
|
16
27
|
function sandboxFor(params, fallback = "read-only") {
|
|
@@ -33,6 +44,8 @@ export class HeadlessBackend {
|
|
|
33
44
|
this.runtime = runtime;
|
|
34
45
|
this.publish = publish;
|
|
35
46
|
this.epoch = randomUUID();
|
|
47
|
+
// Same headroom as desktop's FEDERATION_RESPONSE_BYTE_BUDGET.
|
|
48
|
+
this.replayByteBudget = FEDERATION_MAX_FRAME_BYTES - 64 * 1024;
|
|
36
49
|
this.attentionViews = new Map();
|
|
37
50
|
this.file = path.join(path.dirname(runtime.store.directory), "threads.json");
|
|
38
51
|
try {
|
|
@@ -94,6 +107,10 @@ export class HeadlessBackend {
|
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
event(message) {
|
|
110
|
+
if (message.method === "headless/turnStarted") {
|
|
111
|
+
this.titleFromFirstPrompt(message.params);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
97
114
|
if (message.method === "headless/jobChanged") {
|
|
98
115
|
this.syncJobs([message.params]);
|
|
99
116
|
if (message.params.threadId) this.forgetSeenAttentionMember(message.params.threadId);
|
|
@@ -149,7 +166,14 @@ export class HeadlessBackend {
|
|
|
149
166
|
const messages = history.replay.messages.filter((message) => message.role === "assistant" && message.turn?.id === params.turnId);
|
|
150
167
|
return { message: messages.find((message) => params.itemId ? message.id === params.itemId : message.text === params.text) || null };
|
|
151
168
|
}
|
|
152
|
-
case "readThread":
|
|
169
|
+
case "readThread": {
|
|
170
|
+
// Viewers take the newest page as returned. Page and fit it to one
|
|
171
|
+
// frame here, as desktop owners do.
|
|
172
|
+
const history = await this.readThread(params.threadId);
|
|
173
|
+
return { ...history, replay: fitThreadReplayWithinByteBudget({ cursorIdSpace: "entry-id",
|
|
174
|
+
replay: pageThreadReplay(history.replay, params), maxBytes: this.replayByteBudget,
|
|
175
|
+
measureBytes: (replay) => Buffer.byteLength(JSON.stringify({ ...history, replay }), "utf8") }) };
|
|
176
|
+
}
|
|
153
177
|
case "supportsMessagingPdfTools": return params.backend === "codex" && this.owned(params.threadId).messagingFileTools === true;
|
|
154
178
|
case "startThread": return this.startThread(params);
|
|
155
179
|
case "materializeDirectoryLaunchpad": return this.materializeLaunchpad(params, envelope);
|
|
@@ -186,9 +210,8 @@ export class HeadlessBackend {
|
|
|
186
210
|
const thread = this.owned(params.threadId);
|
|
187
211
|
const title = params.title || params.name;
|
|
188
212
|
if (typeof title !== "string" || !title.trim() || title.length > 200) throw new Error("Invalid title");
|
|
189
|
-
|
|
190
|
-
this.
|
|
191
|
-
this.invalidate(thread.id);
|
|
213
|
+
thread.updatedAt = Date.now();
|
|
214
|
+
this.setTitle(thread, title.trim(), "explicit");
|
|
192
215
|
return { backend: "codex", threadId: thread.id, title: thread.title };
|
|
193
216
|
}
|
|
194
217
|
case "markThreadSeen": {
|
|
@@ -226,7 +249,35 @@ export class HeadlessBackend {
|
|
|
226
249
|
}
|
|
227
250
|
}
|
|
228
251
|
|
|
229
|
-
invalidate(threadId
|
|
252
|
+
invalidate(threadId, sourceMethod) {
|
|
253
|
+
this.publish({ backend: "codex", notification: { method: "navigation/invalidated", params: { threadId, ...(sourceMethod ? { sourceMethod } : {}) } } }, "navigation");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// threads.json is the only store for a title. The outpost never sets the
|
|
257
|
+
// Codex thread name.
|
|
258
|
+
setTitle(thread, title, titleSource) {
|
|
259
|
+
Object.assign(thread, { title, titleSource });
|
|
260
|
+
this.persist();
|
|
261
|
+
this.publish({ backend: "codex", notification: { method: "thread/name/updated",
|
|
262
|
+
params: { threadId: thread.id, threadName: title, titleSource } } }, "transcript");
|
|
263
|
+
this.invalidate(thread.id, "thread/name/updated");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// The outpost has no helper model, so a thread is named as desktop first
|
|
267
|
+
// names one: the shortened first user message. Any rename wins over it.
|
|
268
|
+
deriveTitle(id, text) {
|
|
269
|
+
const thread = this.threads.get(id);
|
|
270
|
+
if (!thread || thread.archived || thread.titleSource !== "fallback" || typeof text !== "string") return;
|
|
271
|
+
const title = shortenDerivedThreadTitle(text)?.slice(0, 200);
|
|
272
|
+
if (title) this.setTitle(thread, title, "derived");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
titleFromFirstPrompt({ threadId, jobId, prompt }) {
|
|
276
|
+
// A later turn is not the first message. readThread names that thread
|
|
277
|
+
// from Codex's preview instead.
|
|
278
|
+
const earlier = [...this.runtime.store.jobs.values()].some((job) => job.threadId === threadId && job.id !== jobId && job.turnId);
|
|
279
|
+
if (!earlier) this.deriveTitle(threadId, prompt);
|
|
280
|
+
}
|
|
230
281
|
|
|
231
282
|
async materializeLaunchpad(params, envelope) {
|
|
232
283
|
const cwd = this.runtime.projects.resolve(params.directoryKey);
|
|
@@ -328,6 +379,9 @@ export class HeadlessBackend {
|
|
|
328
379
|
}
|
|
329
380
|
}
|
|
330
381
|
const messages = entries.filter((entry) => entry.type === "message");
|
|
382
|
+
// Names threads created before derived titles, and imported threads.
|
|
383
|
+
this.deriveTitle(id, typeof result.thread?.preview === "string" && result.thread.preview.trim()
|
|
384
|
+
? result.thread.preview : messages.find((item) => item.role === "user")?.text);
|
|
331
385
|
return { backend: "codex", threadId: id, fetchedAt: Date.now(), threadStatus: this.summary(this.owned(id)).threadStatus,
|
|
332
386
|
replay: { entries, messages, lastUserMessage: messages.filter((item) => item.role === "user").at(-1)?.text,
|
|
333
387
|
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
|
@@ -151,6 +151,8 @@ export class Runtime {
|
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
this.save({ ...this.store.get(job.id), turnId: turn.turn.id, status: "running" });
|
|
154
|
+
// The prompt stays in memory: job records never store it.
|
|
155
|
+
this.onEvent?.({ method: "headless/turnStarted", params: { threadId: result.thread.id, jobId: job.id, prompt } });
|
|
154
156
|
if (this.earlyCompletion?.turn?.id === turn.turn.id) {
|
|
155
157
|
this.complete(this.earlyCompletion);
|
|
156
158
|
} else if (["completed", "failed", "interrupted"].includes(turn.turn.status)) {
|