@aeondave/pi-persona 1.17.1 → 1.17.4
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/AGENTS.md +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +47 -18
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
package/src/engine/worktree.ts
CHANGED
|
@@ -76,11 +76,14 @@ export async function worktreePreflight(root: string, exec: GitExec = defaultGit
|
|
|
76
76
|
return { ok: true };
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** Capture the complete post-run worktree state as a portable unified diff
|
|
80
|
-
*
|
|
79
|
+
/** Capture the complete post-run worktree state as a portable unified diff from its pinned
|
|
80
|
+
* starting commit. Files newly created by a coding leg are added with `git diff --no-index`.
|
|
81
81
|
* Exit code 1 is the normal "files differ" result for --no-index. */
|
|
82
|
-
export async function captureWorktreeArtifact(root: string, exec: GitExec = defaultGitExec): Promise<WorktreeArtifact> {
|
|
83
|
-
|
|
82
|
+
export async function captureWorktreeArtifact(root: string, baseCommit: string, exec: GitExec = defaultGitExec): Promise<WorktreeArtifact> {
|
|
83
|
+
if (!baseCommit.trim()) {
|
|
84
|
+
return { ok: false, error: "could not export the isolated worktree diff: starting commit is missing" };
|
|
85
|
+
}
|
|
86
|
+
const tracked = await exec(["-C", root, "diff", "--no-ext-diff", "--binary", baseCommit, "--"]);
|
|
84
87
|
if (tracked.code !== 0) {
|
|
85
88
|
return { ok: false, error: `could not export the isolated worktree diff: ${tracked.stderr.trim() || `git exited ${tracked.code}`}` };
|
|
86
89
|
}
|
|
@@ -107,8 +110,9 @@ export async function captureWorktreeArtifact(root: string, exec: GitExec = defa
|
|
|
107
110
|
return { ok: true, diff: parts.join("\n") };
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
/** Create a detached worktree off `root`'s HEAD,
|
|
111
|
-
|
|
113
|
+
/** Create a detached worktree off `root`'s HEAD, pin its starting commit, run
|
|
114
|
+
* `body(worktreeDir, baseCommit)`, then force-remove it. */
|
|
115
|
+
export async function withWorktree<T>(root: string, exec: GitExec, body: (dir: string, baseCommit: string) => Promise<T>): Promise<T> {
|
|
112
116
|
const preflight = await worktreePreflight(root, exec);
|
|
113
117
|
if (!preflight.ok) throw new Error(preflight.error);
|
|
114
118
|
const dir = await mkdtemp(join(tmpdir(), "pi-persona-wt-"));
|
|
@@ -120,7 +124,13 @@ export async function withWorktree<T>(root: string, exec: GitExec, body: (dir: s
|
|
|
120
124
|
throw new Error(`could not create git worktree: ${add.stderr.trim() || `git exited ${add.code}`}`);
|
|
121
125
|
}
|
|
122
126
|
try {
|
|
123
|
-
|
|
127
|
+
const initial = await exec(["-C", dir, "rev-parse", "--verify", "HEAD^{commit}"]);
|
|
128
|
+
const baseCommit = initial.stdout.trim();
|
|
129
|
+
if (initial.code !== 0 || !baseCommit) {
|
|
130
|
+
const reason = initial.stderr.trim() || (initial.code !== 0 ? `git exited ${initial.code}` : "Git returned an empty commit id");
|
|
131
|
+
throw new Error(`could not resolve the initial worktree commit: ${reason}`);
|
|
132
|
+
}
|
|
133
|
+
return await body(dir, baseCommit);
|
|
124
134
|
} finally {
|
|
125
135
|
// Remove the worktree registration, then the dir (force: it may carry the agent's edits).
|
|
126
136
|
// Never throw: cleanup must not replace the body's result (or its error) — `defaultGitExec`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Signed offer wakes project retained ledger data; the envelope is never its authority. */
|
|
2
|
+
import { attributePeer } from "../core/fence.ts";
|
|
3
|
+
import { collaborationOfferStatus } from "./collaboration.ts";
|
|
4
|
+
import { frameSigningPayload, type ExocomOffer, type ExocomSemanticFrame } from "./envelope.ts";
|
|
5
|
+
import type { LedgerState } from "./ledger.ts";
|
|
6
|
+
|
|
7
|
+
export function collaborationWriteIntent(frame: ExocomSemanticFrame, state: LedgerState): boolean {
|
|
8
|
+
if (frame.kind === "claim" || frame.kind === "task_create") return frame.write_set.length > 0;
|
|
9
|
+
return frame.kind === "task_take" && (state.collaboration?.tasks.find((task) => task.created.work_key === frame.work_key)?.created.write_set.length ?? 0) > 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function deliverCollaborationOffer(
|
|
13
|
+
frame: ExocomOffer, state: LedgerState, sessionId: string, label: string, now: number,
|
|
14
|
+
): { accepted: true; notice?: string } | { accepted: false; reason: string } {
|
|
15
|
+
if (frame.to_session !== sessionId) return { accepted: false, reason: "offer addressed to another session" };
|
|
16
|
+
const stored = state.collaboration?.offers.find((offer) => offer.offer_id === frame.offer_id);
|
|
17
|
+
if (!stored || frameSigningPayload(stored) !== frameSigningPayload(frame)) {
|
|
18
|
+
return { accepted: false, reason: "offer is not durable with this sender and payload" };
|
|
19
|
+
}
|
|
20
|
+
const tasks = state.collaboration?.tasks ?? [];
|
|
21
|
+
if (collaborationOfferStatus(stored, tasks, now) !== "open") return { accepted: true };
|
|
22
|
+
const task = tasks.find((candidate) => candidate.created.work_key === stored.work_key && candidate.created.msg_id === stored.task_id);
|
|
23
|
+
if (!task) return { accepted: true };
|
|
24
|
+
return { accepted: true, notice: [
|
|
25
|
+
`[pi-persona] Optional peer invitation · task=${stored.work_key} · offer=${stored.offer_id} · expires ${stored.expires_at}`,
|
|
26
|
+
"This creates no response obligation or permission. Inspect the task and take it voluntarily only within your authorized scope; otherwise continue your work.",
|
|
27
|
+
attributePeer(frame.from_session === task.created.from_session ? label : task.created.from_session, task.created.description),
|
|
28
|
+
].join("\n") };
|
|
29
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Voluntary peer work. Pure domain rules; claims and persistence stay in the ledger transaction. */
|
|
2
|
+
import { writeSetPathError } from "../core/ownership.ts";
|
|
3
|
+
import { isSemanticFrame, type ExocomSemanticFrame, type ExocomCollaborationFrame, type ExocomOffer, type ExocomProgress, type ExocomPublish, type ExocomTaskCreate, type ExocomTaskFinish, type ExocomTaskTake } from "./envelope.ts";
|
|
4
|
+
|
|
5
|
+
export interface CollaborationTask {
|
|
6
|
+
created: ExocomTaskCreate;
|
|
7
|
+
taken?: ExocomTaskTake;
|
|
8
|
+
progress?: ExocomProgress;
|
|
9
|
+
finished?: ExocomTaskFinish;
|
|
10
|
+
/** Loss of the owning registry lease is visible and never permits automatic pickup. */
|
|
11
|
+
stale?: true;
|
|
12
|
+
/** Receiver clock at the first lease-loss reconciliation, or release timestamp. */
|
|
13
|
+
staleAt?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface CollaborationState { tasks: CollaborationTask[]; offers: ExocomOffer[]; evidence: ExocomPublish[]; }
|
|
16
|
+
export const COLLABORATION_LIMITS = { tasks: 64, offers: 64, evidence: 64, maxBytes: 131_072 } as const;
|
|
17
|
+
export function emptyCollaboration(): CollaborationState { return { tasks: [], offers: [], evidence: [] }; }
|
|
18
|
+
export function collaborationStatus(task: CollaborationTask): "open" | "taken" | "done" | "abandoned" | "stale" {
|
|
19
|
+
return task.finished?.outcome ?? (task.stale ? "stale" : task.taken ? "taken" : "open");
|
|
20
|
+
}
|
|
21
|
+
export function offerStatus(offer: ExocomOffer, now: number): "open" | "expired" {
|
|
22
|
+
return Date.parse(offer.expires_at) <= now ? "expired" : "open";
|
|
23
|
+
}
|
|
24
|
+
/** Time expiry is authoritative even after the exact task generation becomes unavailable. */
|
|
25
|
+
export function collaborationOfferStatus(offer: ExocomOffer, tasks: readonly CollaborationTask[], now: number): "open" | "expired" | "stale" {
|
|
26
|
+
if (offerStatus(offer, now) === "expired") return "expired";
|
|
27
|
+
const task = tasks.find((candidate) => candidate.created.work_key === offer.work_key && candidate.created.msg_id === offer.task_id);
|
|
28
|
+
return task && collaborationStatus(task) === "open" ? "open" : "stale";
|
|
29
|
+
}
|
|
30
|
+
function bounded(state: CollaborationState, reserveLifecycle = true): boolean {
|
|
31
|
+
// Local ledger lifecycle events are unsigned. Reserve their largest identity metadata and a
|
|
32
|
+
// minimal terminal note, as well as lease loss, before admitting optional prose. A full note
|
|
33
|
+
// can hit quota; taking work, finishing with a short note and release must remain available.
|
|
34
|
+
const reserved = { ...state, tasks: state.tasks.map((task) => {
|
|
35
|
+
if (task.finished) return task;
|
|
36
|
+
const future = { work_key: task.created.work_key, task_id: task.created.msg_id, from_session: "x".repeat(128),
|
|
37
|
+
from_name: "界".repeat(256), msg_id: "x".repeat(128), ts: "9999-12-31T23:59:59.999Z" };
|
|
38
|
+
return { ...task, stale: true, staleAt: -Number.MAX_VALUE,
|
|
39
|
+
taken: task.taken ?? { ...future, kind: "task_take" }, finished: { ...future, kind: "task_finish", outcome: "abandoned", note: "x" } };
|
|
40
|
+
}) };
|
|
41
|
+
return state.tasks.length <= COLLABORATION_LIMITS.tasks && state.offers.length <= COLLABORATION_LIMITS.offers
|
|
42
|
+
&& state.evidence.length <= COLLABORATION_LIMITS.evidence && Buffer.byteLength(JSON.stringify(reserveLifecycle ? reserved : state), "utf8") <= COLLABORATION_LIMITS.maxBytes;
|
|
43
|
+
}
|
|
44
|
+
/** Retained facts outlive the rolling transport-id window; they must never acquire duplicate identities. */
|
|
45
|
+
export function retainedCollaborationEvent(state: CollaborationState, msgId: string): ExocomSemanticFrame | undefined {
|
|
46
|
+
for (const task of state.tasks) {
|
|
47
|
+
for (const event of [task.created, task.taken, task.progress, task.finished]) if (event?.msg_id === msgId) return event;
|
|
48
|
+
}
|
|
49
|
+
return state.offers.find((event) => event.msg_id === msgId) ?? state.evidence.find((event) => event.msg_id === msgId);
|
|
50
|
+
}
|
|
51
|
+
type Transition = { ok: true; state: CollaborationState } | { ok: false; error: string };
|
|
52
|
+
function transition(state: CollaborationState, event: ExocomCollaborationFrame | ExocomProgress, reserveLifecycle: boolean): Transition {
|
|
53
|
+
if (event.kind === "progress" && event.task_id === undefined) return { ok: true, state };
|
|
54
|
+
const task = state.tasks.find((value) => value.created.work_key === event.work_key);
|
|
55
|
+
let next = state;
|
|
56
|
+
if (event.kind === "task_create") {
|
|
57
|
+
if (task) return { ok: false, error: "task work_key already exists" };
|
|
58
|
+
for (const path of event.write_set) {
|
|
59
|
+
const error = writeSetPathError(path);
|
|
60
|
+
if (error) return { ok: false, error: `task write_set path is ${error}` };
|
|
61
|
+
}
|
|
62
|
+
next = { ...state, tasks: [...state.tasks, { created: event }] };
|
|
63
|
+
} else {
|
|
64
|
+
if (!task) return { ok: false, error: "unknown collaboration task" };
|
|
65
|
+
if (event.task_id !== task.created.msg_id) return { ok: false, error: "task_id does not match this work key's retained generation" };
|
|
66
|
+
if (event.kind === "publish") next = { ...state, evidence: [...state.evidence, event] };
|
|
67
|
+
else if (event.kind === "offer") {
|
|
68
|
+
if (state.offers.some((offer) => offer.offer_id === event.offer_id)) return { ok: false, error: "offer_id already exists" };
|
|
69
|
+
if (event.to_session === event.from_session) return { ok: false, error: "offer cannot target self" };
|
|
70
|
+
if (collaborationStatus(task) !== "open") return { ok: false, error: "offers require an open task" };
|
|
71
|
+
next = { ...state, offers: [...state.offers, event] };
|
|
72
|
+
} else {
|
|
73
|
+
const status = collaborationStatus(task);
|
|
74
|
+
if (event.kind === "task_take") {
|
|
75
|
+
if (status !== "open") return { ok: false, error: `task is ${status}; pickup refused` };
|
|
76
|
+
next = { ...state, tasks: state.tasks.map((value) => value === task ? { ...task, taken: event } : value) };
|
|
77
|
+
} else {
|
|
78
|
+
if (status !== "taken" || task.taken?.from_session !== event.from_session) return { ok: false, error: "only the active task taker may report progress or finish" };
|
|
79
|
+
if (event.kind === "progress" && Date.parse(event.ts) < Date.parse(task.progress?.ts ?? task.taken.ts)) return { ok: false, error: "task progress predates its latest observation" };
|
|
80
|
+
const updated = event.kind === "progress" ? { ...task, progress: event } : { ...task, finished: event };
|
|
81
|
+
next = { ...state, tasks: state.tasks.map((value) => value === task ? updated : value) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return bounded(next, reserveLifecycle) ? { ok: true, state: next } : { ok: false, error: "collaboration retention budget reached; shorten optional report text" };
|
|
86
|
+
}
|
|
87
|
+
export function applyCollaborationEvent(state: CollaborationState, event: ExocomCollaborationFrame | ExocomProgress): Transition {
|
|
88
|
+
return transition(state, event, true);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function staleCollaboration(state: CollaborationState, isLive: (session: string) => boolean, now: number): CollaborationState {
|
|
92
|
+
return { ...state, tasks: state.tasks.map((task) => task.finished || task.stale || isLive(task.taken?.from_session ?? task.created.from_session)
|
|
93
|
+
? task : { ...task, stale: true, staleAt: now }) };
|
|
94
|
+
}
|
|
95
|
+
export function releaseCollaboration(state: CollaborationState, session: string, workKey: string, now: number): CollaborationState {
|
|
96
|
+
return { ...state, tasks: state.tasks.map((task) => task.created.work_key === workKey && !task.finished && !task.stale
|
|
97
|
+
&& (task.taken?.from_session ?? task.created.from_session) === session ? { ...task, stale: true, staleAt: now } : task) };
|
|
98
|
+
}
|
|
99
|
+
export function pruneCollaboration(state: CollaborationState, now: number, ttlMs: number, isLive: (session: string) => boolean): CollaborationState {
|
|
100
|
+
const reconciled = staleCollaboration(state, isLive, now);
|
|
101
|
+
const tasks = reconciled.tasks.filter((task) => {
|
|
102
|
+
const terminalAt = task.finished ? Date.parse(task.finished.ts) : task.staleAt;
|
|
103
|
+
return terminalAt === undefined || now - terminalAt <= ttlMs;
|
|
104
|
+
});
|
|
105
|
+
const retained = new Set(tasks.map((task) => task.created.work_key));
|
|
106
|
+
return {
|
|
107
|
+
tasks,
|
|
108
|
+
offers: state.offers.filter((offer) => retained.has(offer.work_key) && now - Date.parse(offer.expires_at) <= ttlMs),
|
|
109
|
+
evidence: state.evidence.filter((item) => retained.has(item.work_key) && now - Date.parse(item.ts) <= ttlMs),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Reconstruct snapshots through the same rules. No malformed ownership record is silently skipped. */
|
|
114
|
+
export function parseCollaboration(raw: unknown): CollaborationState | undefined {
|
|
115
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
116
|
+
const value = raw as CollaborationState;
|
|
117
|
+
if (!Array.isArray(value.tasks) || !Array.isArray(value.offers) || !Array.isArray(value.evidence) || !bounded(value)) return undefined;
|
|
118
|
+
let state = emptyCollaboration();
|
|
119
|
+
const seen = new Set<string>();
|
|
120
|
+
function replay(event: unknown, kind: string): boolean {
|
|
121
|
+
if (!isSemanticFrame(event) || event.kind !== kind || seen.has(event.msg_id)) return false;
|
|
122
|
+
if (!(event.kind === "task_create" || event.kind === "task_take" || event.kind === "task_finish" || event.kind === "progress" || event.kind === "offer" || event.kind === "publish")) return false;
|
|
123
|
+
// Definitions must precede historical offers. Replay checks physical bounds; reserve
|
|
124
|
+
// future lifecycle only on the final state, once completed tasks are terminal again.
|
|
125
|
+
const applied = transition(state, event, false);
|
|
126
|
+
if (!applied.ok) return false;
|
|
127
|
+
seen.add(event.msg_id);
|
|
128
|
+
state = applied.state;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
// All definitions precede offers/evidence; a finished task's historical offer is valid.
|
|
132
|
+
for (const task of value.tasks) {
|
|
133
|
+
if (!task || typeof task !== "object" || !replay(task.created, "task_create")) return undefined;
|
|
134
|
+
}
|
|
135
|
+
for (const offer of value.offers) if (!replay(offer, "offer")) return undefined;
|
|
136
|
+
for (const task of value.tasks) {
|
|
137
|
+
for (const [event, kind] of [[task.taken, "task_take"], [task.progress, "progress"], [task.finished, "task_finish"]] as const) {
|
|
138
|
+
if (event !== undefined && (event.work_key !== task.created.work_key || !replay(event, kind))) return undefined;
|
|
139
|
+
}
|
|
140
|
+
if (task.stale !== undefined && task.stale !== true) return undefined;
|
|
141
|
+
if (task.stale) {
|
|
142
|
+
if (task.finished || typeof task.staleAt !== "number" || !Number.isFinite(task.staleAt)) return undefined;
|
|
143
|
+
state = releaseCollaboration(state, task.taken?.from_session ?? task.created.from_session, task.created.work_key, task.staleAt);
|
|
144
|
+
} else if (task.staleAt !== undefined) return undefined;
|
|
145
|
+
}
|
|
146
|
+
for (const evidence of value.evidence) if (!replay(evidence, "publish")) return undefined;
|
|
147
|
+
return bounded(state) ? state : undefined;
|
|
148
|
+
}
|
package/src/exocom/envelope.ts
CHANGED
|
@@ -31,13 +31,20 @@ export interface ExocomAnswer {
|
|
|
31
31
|
}
|
|
32
32
|
export interface ExocomProgress {
|
|
33
33
|
kind: "progress"; work_key: string; from_session: string; from_name: string;
|
|
34
|
-
note: string; msg_id: string; ts: string; signature?: string;
|
|
34
|
+
note: string; msg_id: string; ts: string; signature?: string; task_id?: string;
|
|
35
35
|
}
|
|
36
36
|
export interface ExocomRelease {
|
|
37
37
|
kind: "release"; work_key: string; from_session: string; from_name: string;
|
|
38
38
|
msg_id: string; ts: string; signature?: string;
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
interface CollaborationFrame { work_key: string; from_session: string; from_name: string; msg_id: string; ts: string; signature?: string; }
|
|
41
|
+
export interface ExocomTaskCreate extends CollaborationFrame { kind: "task_create"; description: string; write_set: string[]; }
|
|
42
|
+
export interface ExocomTaskTake extends CollaborationFrame { kind: "task_take"; task_id: string; }
|
|
43
|
+
export interface ExocomTaskFinish extends CollaborationFrame { kind: "task_finish"; task_id: string; outcome: "done" | "abandoned"; note: string; }
|
|
44
|
+
export interface ExocomOffer extends CollaborationFrame { kind: "offer"; task_id: string; offer_id: string; to_session?: string; expires_at: string; }
|
|
45
|
+
export interface ExocomPublish extends CollaborationFrame { kind: "publish"; task_id: string; title: string; excerpt: string; }
|
|
46
|
+
export type ExocomCollaborationFrame = ExocomTaskCreate | ExocomTaskTake | ExocomTaskFinish | ExocomOffer | ExocomPublish;
|
|
47
|
+
export type ExocomSemanticFrame = ExocomClaim | ExocomAsk | ExocomAnswer | ExocomProgress | ExocomRelease | ExocomCollaborationFrame;
|
|
41
48
|
export type ExocomFrame = ExocomMessage | ExocomBye | ExocomAck | ExocomNack | ExocomSemanticFrame;
|
|
42
49
|
|
|
43
50
|
const str = (v: unknown): v is string => typeof v === "string";
|
|
@@ -73,10 +80,27 @@ export function isSemanticFrame(v: unknown): v is ExocomSemanticFrame {
|
|
|
73
80
|
case "progress":
|
|
74
81
|
return token(o.work_key) && token(o.from_session) && metadata(o.from_name, 256)
|
|
75
82
|
&& bounded(o.note, 4_096) && token(o.msg_id) && timestamp(o.ts)
|
|
76
|
-
&& optionalBounded(o.signature, 512);
|
|
83
|
+
&& optionalBounded(o.signature, 512) && (o.task_id === undefined || token(o.task_id));
|
|
77
84
|
case "release":
|
|
78
85
|
return token(o.work_key) && token(o.from_session) && metadata(o.from_name, 256)
|
|
79
86
|
&& token(o.msg_id) && timestamp(o.ts) && optionalBounded(o.signature, 512);
|
|
87
|
+
case "task_create":
|
|
88
|
+
case "task_take":
|
|
89
|
+
case "task_finish":
|
|
90
|
+
case "offer":
|
|
91
|
+
case "publish": {
|
|
92
|
+
if (!token(o.work_key) || !token(o.from_session) || !metadata(o.from_name, 256)
|
|
93
|
+
|| !token(o.msg_id) || !timestamp(o.ts) || !optionalBounded(o.signature, 512)) return false;
|
|
94
|
+
if (o.kind === "task_create") return bounded(o.description, 2_048) && Array.isArray(o.write_set)
|
|
95
|
+
&& (o.write_set.length === 0 || writeSet(o.write_set));
|
|
96
|
+
if (!token(o.task_id)) return false;
|
|
97
|
+
if (o.kind === "task_finish") return (o.outcome === "done" || o.outcome === "abandoned") && bounded(o.note, 4_096);
|
|
98
|
+
if (o.kind === "offer") return token(o.offer_id) && (o.to_session === undefined || token(o.to_session))
|
|
99
|
+
&& timestamp(o.expires_at) && Date.parse(o.expires_at) > Date.parse(o.ts)
|
|
100
|
+
&& Date.parse(o.expires_at) - Date.parse(o.ts) <= 86_400_000;
|
|
101
|
+
if (o.kind === "publish") return bounded(o.title, 160) && bounded(o.excerpt, 4_096) && Buffer.byteLength(o.excerpt, "utf8") <= 8_192;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
80
104
|
default:
|
|
81
105
|
return false;
|
|
82
106
|
}
|
|
@@ -102,6 +126,11 @@ export function isExocomFrame(v: unknown): v is ExocomFrame {
|
|
|
102
126
|
case "answer":
|
|
103
127
|
case "progress":
|
|
104
128
|
case "release":
|
|
129
|
+
case "task_create":
|
|
130
|
+
case "task_take":
|
|
131
|
+
case "task_finish":
|
|
132
|
+
case "offer":
|
|
133
|
+
case "publish":
|
|
105
134
|
return isSemanticFrame(v);
|
|
106
135
|
default: return false;
|
|
107
136
|
}
|
|
@@ -127,9 +156,19 @@ export function frameSigningPayload(frame: ExocomFrame): string {
|
|
|
127
156
|
frame.ok, frame.evidence, frame.ts]);
|
|
128
157
|
case "progress":
|
|
129
158
|
return JSON.stringify([frame.kind, frame.msg_id, frame.work_key, frame.from_session, frame.from_name,
|
|
130
|
-
frame.note, frame.ts]);
|
|
159
|
+
frame.note, frame.ts, frame.task_id ?? null]);
|
|
131
160
|
case "release":
|
|
132
161
|
return JSON.stringify([frame.kind, frame.msg_id, frame.work_key, frame.from_session, frame.from_name, frame.ts]);
|
|
162
|
+
case "task_create":
|
|
163
|
+
return JSON.stringify([frame.kind, frame.msg_id, frame.work_key, frame.from_session, frame.from_name, frame.description, frame.write_set, frame.ts]);
|
|
164
|
+
case "task_take":
|
|
165
|
+
return JSON.stringify([frame.kind, frame.msg_id, frame.work_key, frame.task_id, frame.from_session, frame.from_name, frame.ts]);
|
|
166
|
+
case "task_finish":
|
|
167
|
+
return JSON.stringify([frame.kind, frame.msg_id, frame.work_key, frame.task_id, frame.from_session, frame.from_name, frame.outcome, frame.note, frame.ts]);
|
|
168
|
+
case "offer":
|
|
169
|
+
return JSON.stringify([frame.kind, frame.msg_id, frame.offer_id, frame.work_key, frame.task_id, frame.from_session, frame.from_name, frame.to_session ?? null, frame.expires_at, frame.ts]);
|
|
170
|
+
case "publish":
|
|
171
|
+
return JSON.stringify([frame.kind, frame.msg_id, frame.work_key, frame.task_id, frame.from_session, frame.from_name, frame.title, frame.excerpt, frame.ts]);
|
|
133
172
|
}
|
|
134
173
|
}
|
|
135
174
|
|
package/src/exocom/gate.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/** Identity metadata does not claim work or settle asks. Persona denials still apply first. */
|
|
2
2
|
const ALLOWED = new Set(["exocom_name", "exocom_answer", "exocom_decline", "exocom_status", "read", "grep", "find", "ls"]);
|
|
3
3
|
|
|
4
|
-
export function constrainedTurnAllows(toolName: string): boolean {
|
|
5
|
-
|
|
4
|
+
export function constrainedTurnAllows(toolName: string, input?: unknown): boolean {
|
|
5
|
+
if (ALLOWED.has(toolName)) return true;
|
|
6
|
+
if (!input || typeof input !== "object") return false;
|
|
7
|
+
const action = (input as { action?: unknown }).action;
|
|
8
|
+
return (toolName === "exocom_task" && (action === "list" || action === "get"))
|
|
9
|
+
|| (toolName === "exocom_publish" && action === "read")
|
|
10
|
+
|| (toolName === "exocom_profile" && action === "get");
|
|
6
11
|
}
|
package/src/exocom/install.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { canCallTool, canParticipateInExocom, EXOCOM_TOOL_NAMES } from "../core/
|
|
|
13
13
|
import type { DelegationNudge } from "../core/nudge.ts";
|
|
14
14
|
import { shortModel } from "../tools/delegate.ts";
|
|
15
15
|
import { boundDisplayRows } from "../ui/presentation.ts";
|
|
16
|
+
import { compactWidget } from "../ui/compact-widget.ts";
|
|
16
17
|
import type { PersonaController } from "../persona/controller.ts";
|
|
17
18
|
import { type InstanceDescriptor, type PeerDescriptor } from "../telemetry/contract.ts";
|
|
18
19
|
import type { TelemetryProducer } from "../telemetry/producer.ts";
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
sanitizePeerField,
|
|
27
28
|
sendPersonaFollowUp,
|
|
28
29
|
shouldReportHeartbeatFailure,
|
|
30
|
+
toastCause,
|
|
29
31
|
type ExocomQueuedToast,
|
|
30
32
|
} from "../extension/shared.ts";
|
|
31
33
|
import { SeenMessages, SenderBudget } from "./guards.ts";
|
|
@@ -53,6 +55,9 @@ import type { ExocomSemanticFrame } from "./envelope.ts";
|
|
|
53
55
|
import { prune as pruneExocom, type RegistryEntry } from "./registry.ts";
|
|
54
56
|
import { registerExocomTools } from "../tools/exocom.ts";
|
|
55
57
|
import { registerExocomWorkTools } from "../tools/exocom-work.ts";
|
|
58
|
+
import { registerExocomCollaborationTools } from "../tools/exocom-collaboration.ts";
|
|
59
|
+
import { registerExocomProfileTool } from "../tools/exocom-profile.ts";
|
|
60
|
+
import { collaborationWriteIntent, deliverCollaborationOffer } from "./collaboration-delivery.ts";
|
|
56
61
|
import type { SessionIdentity } from "../extension/identity.ts";
|
|
57
62
|
import { WriteWarnings } from "./write-guard.ts";
|
|
58
63
|
|
|
@@ -236,28 +241,23 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
236
241
|
}
|
|
237
242
|
|
|
238
243
|
function telemetrySessionId(ctx: ExtensionContext): string {
|
|
239
|
-
return
|
|
244
|
+
return ctx.sessionManager.getSessionId();
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
function currentTelemetryInstance(ctx: ExtensionContext): InstanceDescriptor {
|
|
243
|
-
const runtimeCtx = ctx as ExtensionContext & {
|
|
244
|
-
sessionManager?: { getSessionId?: () => string };
|
|
245
|
-
isIdle?: () => boolean;
|
|
246
|
-
getContextUsage?: () => { percent?: number } | undefined;
|
|
247
|
-
};
|
|
248
248
|
const personaName = host.controller.activePersona?.name ?? "";
|
|
249
249
|
const displayName = exocomSelfStatusLabel(host.identity.chosen, host.identity.name, personaName);
|
|
250
250
|
return {
|
|
251
251
|
displayName,
|
|
252
252
|
persona: host.controller.activePersona?.name ?? "",
|
|
253
253
|
model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "",
|
|
254
|
-
status:
|
|
254
|
+
status: ctx.isIdle() ? "idle" : "active",
|
|
255
255
|
pid: process.pid,
|
|
256
256
|
// Clamped HERE, not merely in the producer's `sanitizeInstance`: that one runs only for
|
|
257
257
|
// `instance.started`, so an out-of-range reading would ride `instance.updated` and
|
|
258
258
|
// `instance.heartbeat` out to consumers, where the contract rejects the WHOLE event over
|
|
259
259
|
// the one bad field.
|
|
260
|
-
contextPercent: Math.max(0, Math.min(100, Math.round(
|
|
260
|
+
contextPercent: Math.max(0, Math.min(100, Math.round(ctx.getContextUsage()?.percent ?? 0))),
|
|
261
261
|
exocomEnabled: exocomPlane !== undefined,
|
|
262
262
|
color: exocomColorFor(displayName),
|
|
263
263
|
};
|
|
@@ -327,7 +327,7 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
327
327
|
return `${quiet ? "💤" : "📡"} ${name}${persona ? ` (${persona})` : ""} · ${shortModel(model) || "?"} · ctx ${contextPct}% · recv ${exocomPlane?.receivedFromPeer(p.session_id) ?? 0} · sent ${exocomPlane?.sentToPeer(p.session_id) ?? 0}${workspaceBit}`;
|
|
328
328
|
});
|
|
329
329
|
const lines = boundDisplayRows(local, peerRows, 7, "exocom_list for the full pool");
|
|
330
|
-
host.lastCtx.ui.setWidget("persona-exocom", lines, { placement: "aboveEditor" });
|
|
330
|
+
host.lastCtx.ui.setWidget("persona-exocom", compactWidget(lines), { placement: "aboveEditor" });
|
|
331
331
|
} catch {
|
|
332
332
|
/* cosmetic — the widget is best-effort */
|
|
333
333
|
}
|
|
@@ -411,7 +411,7 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
411
411
|
exocomSeen = new SeenMessages({ ttlMs: EXOCOM.SEEN_TTL_MS });
|
|
412
412
|
exocomLedgerFile = ledgerPath(agentDir, hash);
|
|
413
413
|
exocomWaitNotifier = new IdleCoalescingNotifier<string>({
|
|
414
|
-
isIdle: () => canDeliverPersonaNotification(host.orchestrating, host.processingDeferredOrchestration, host.lastCtx?.isIdle
|
|
414
|
+
isIdle: () => canDeliverPersonaNotification(host.orchestrating, host.processingDeferredOrchestration, host.lastCtx?.isIdle() === true),
|
|
415
415
|
deliver: (message) => sendPersonaFollowUp(pi, message, "pi-persona"),
|
|
416
416
|
render: (items) => items.join("\n\n"),
|
|
417
417
|
setTimer: (fn, ms) => { const h = setTimeout(fn, ms); h.unref?.(); return h; },
|
|
@@ -422,7 +422,7 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
422
422
|
// pi.sendMessage (a distinct, labellable custom message) rather than pi.sendUserMessage
|
|
423
423
|
// (mirrors the bridge's own `sendFollowUp` for inbound cross-process text).
|
|
424
424
|
exocomNotifier = new IdleCoalescingNotifier<string>({
|
|
425
|
-
isIdle: () => canDeliverPersonaNotification(host.orchestrating, host.processingDeferredOrchestration, host.lastCtx?.isIdle
|
|
425
|
+
isIdle: () => canDeliverPersonaNotification(host.orchestrating, host.processingDeferredOrchestration, host.lastCtx?.isIdle() === true),
|
|
426
426
|
deliver: (message) => sendPersonaFollowUp(pi, message, "exocom_received"),
|
|
427
427
|
// A burst of independent peers must not turn one idle wake into an unbounded model
|
|
428
428
|
// payload. Each item is already attributed and peer-fenced by buildInboundDelivery;
|
|
@@ -535,7 +535,7 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
535
535
|
if (!plane) throw new Error("exocom is not active for this persona");
|
|
536
536
|
if (!exocomLedgerFile) throw new Error("exocom ledger is not available");
|
|
537
537
|
let target: DisplayPeer | undefined;
|
|
538
|
-
if (frame.kind === "ask") {
|
|
538
|
+
if (frame.kind === "ask" || (frame.kind === "offer" && frame.to_session !== undefined)) {
|
|
539
539
|
target = plane.listPeers().find((peer) => peer.session_id === frame.to_session);
|
|
540
540
|
if (!target) throw new Error(`exocom: unknown peer session "${frame.to_session}"`);
|
|
541
541
|
} else if (frame.kind === "answer") {
|
|
@@ -549,7 +549,14 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
549
549
|
}
|
|
550
550
|
}
|
|
551
551
|
const prune = currentLedgerPruneOptions();
|
|
552
|
-
|
|
552
|
+
if (collaborationWriteIntent(frame, currentLedgerState())
|
|
553
|
+
&& (exocomScope?.joined === true || (host.controller.capabilities && !canCallTool(host.controller.capabilities, "exocom_claim")))) {
|
|
554
|
+
throw new Error("exocom: this session cannot claim write paths in the selected scope");
|
|
555
|
+
}
|
|
556
|
+
const applied = commitLedgerEvent(exocomLedgerFile, frame, {
|
|
557
|
+
...(prune ? { prune } : {}),
|
|
558
|
+
canClaim: exocomScope?.joined !== true && (!host.controller.capabilities || canCallTool(host.controller.capabilities, "exocom_claim")),
|
|
559
|
+
});
|
|
553
560
|
if (!applied.ok) throw new Error(`exocom: ${applied.error}`);
|
|
554
561
|
if (applied.duplicate === true) return { msg_id: frame.msg_id };
|
|
555
562
|
// Claims/progress/releases are authoritative in the shared ledger and need no postcard.
|
|
@@ -561,7 +568,7 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
561
568
|
} catch { /* the durable result is still recoverable from the ledger */ }
|
|
562
569
|
return { msg_id: frame.msg_id, peerWakeDeferred: true };
|
|
563
570
|
}
|
|
564
|
-
if (target && (frame.kind === "ask" || frame.kind === "answer")) {
|
|
571
|
+
if (target && (frame.kind === "ask" || frame.kind === "answer" || frame.kind === "offer")) {
|
|
565
572
|
try {
|
|
566
573
|
await plane.sendSemantic(target.target, frame);
|
|
567
574
|
} catch {
|
|
@@ -656,7 +663,7 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
656
663
|
exocomNotifier?.notify(decision.deliver);
|
|
657
664
|
// Never inject peer text into a live model turn. A compact human-only toast makes
|
|
658
665
|
// receipt visible while a long tool loop keeps the follow-up idle-gated.
|
|
659
|
-
if (host.lastCtx?.isIdle
|
|
666
|
+
if (host.lastCtx?.isIdle() !== true) {
|
|
660
667
|
exocomToastNotifier?.notify({ label, inReplyTo: msg.in_reply_to });
|
|
661
668
|
}
|
|
662
669
|
}
|
|
@@ -691,7 +698,15 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
691
698
|
return { accepted: false, reason: error instanceof Error ? error.message : "ledger unavailable" };
|
|
692
699
|
}
|
|
693
700
|
const label = exocomPlane?.humanDisplayLabelFor(fromEntry) ?? fromEntry.session_id;
|
|
694
|
-
if (
|
|
701
|
+
if (fromEntry.workspace_id !== undefined && fromEntry.workspace_id !== scope.scopeWorkspaceId && collaborationWriteIntent(frame, state)) {
|
|
702
|
+
exocomSeen?.forget(frame.from_session, frame.msg_id);
|
|
703
|
+
return { accepted: false, reason: "external-workspace peers cannot take write tasks in the selected scope" };
|
|
704
|
+
}
|
|
705
|
+
if (frame.kind === "offer") {
|
|
706
|
+
const delivery = deliverCollaborationOffer(frame, state, sessionId, label, Date.now());
|
|
707
|
+
if (!delivery.accepted) { exocomSeen?.forget(frame.from_session, frame.msg_id); return delivery; }
|
|
708
|
+
if (delivery.notice) exocomNotifier?.notify(delivery.notice);
|
|
709
|
+
} else if (frame.kind === "ask") {
|
|
695
710
|
if (frame.to_session !== sessionId) {
|
|
696
711
|
exocomSeen?.forget(frame.from_session, frame.msg_id);
|
|
697
712
|
return { accepted: false, reason: "ask addressed to another session" };
|
|
@@ -739,7 +754,7 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
739
754
|
if (shouldReportHeartbeatFailure(exocomHeartbeatFailures)) {
|
|
740
755
|
try {
|
|
741
756
|
host.lastCtx?.ui.notify(
|
|
742
|
-
`exocom: heartbeat failed ${exocomHeartbeatFailures}× in a row (${message}) — peers drop this instance from the pool until it recovers.`,
|
|
757
|
+
`exocom: heartbeat failed ${exocomHeartbeatFailures}× in a row (${toastCause(message)}) — peers drop this instance from the pool until it recovers.`,
|
|
743
758
|
"warning",
|
|
744
759
|
);
|
|
745
760
|
} catch {
|
|
@@ -774,6 +789,19 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
774
789
|
ledger: () => currentLedgerState(),
|
|
775
790
|
labelFor: peerLabelFor,
|
|
776
791
|
});
|
|
792
|
+
registerExocomCollaborationTools(pi, {
|
|
793
|
+
getPlane: () => exocomPlane,
|
|
794
|
+
sessionId: () => sessionId, name: () => host.identity.name, now: () => Date.now(),
|
|
795
|
+
canClaim: () => exocomScope?.joined !== true,
|
|
796
|
+
canCallTool: (name) => !host.controller.capabilities || canCallTool(host.controller.capabilities, name),
|
|
797
|
+
resolveTarget: (target) => {
|
|
798
|
+
if (!exocomPlane) throw new Error("exocom is not active for this persona");
|
|
799
|
+
return exocomPlane.resolvePeer(target).session_id;
|
|
800
|
+
},
|
|
801
|
+
dispatch: dispatchSemantic, ledger: currentLedgerState, labelFor: peerLabelFor,
|
|
802
|
+
targetForSession: (id) => exocomPlane?.listPeers().find((peer) => peer.session_id === id)?.target,
|
|
803
|
+
});
|
|
804
|
+
registerExocomProfileTool(pi, () => exocomPlane);
|
|
777
805
|
} catch (err) {
|
|
778
806
|
// `plane.start()` may have bound its server before a later registry write failed.
|
|
779
807
|
// Teardown while the reference is still owned; this also cancels notifier/timers.
|
|
@@ -782,7 +810,7 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
782
810
|
let surfacedInUi = false;
|
|
783
811
|
if (ctx.hasUI) {
|
|
784
812
|
try {
|
|
785
|
-
ctx.ui.notify(`exocom failed to start: ${message}`, "error");
|
|
813
|
+
ctx.ui.notify(`exocom failed to start: ${toastCause(message)}`, "error");
|
|
786
814
|
surfacedInUi = true;
|
|
787
815
|
} catch {
|
|
788
816
|
/* the session may already be tearing down; stderr remains available */
|
|
@@ -890,6 +918,7 @@ export function installExocom(pi: ExtensionAPI, host: ExocomHost): ExocomInstall
|
|
|
890
918
|
const workKeys = new Set([
|
|
891
919
|
...state.claims.filter((claim) => claim.from_session === exocomSessionId).map((claim) => claim.work_key),
|
|
892
920
|
...state.asks.filter((ask) => ask.from_session === exocomSessionId).map((ask) => ask.work_key),
|
|
921
|
+
...(state.collaboration?.tasks.filter((task) => !task.finished && (task.taken?.from_session ?? task.created.from_session) === exocomSessionId).map((task) => task.created.work_key) ?? []),
|
|
893
922
|
]);
|
|
894
923
|
for (const work_key of workKeys) {
|
|
895
924
|
const prune = currentLedgerPruneOptions();
|