pi-onlyne 0.9.1 → 1.0.0

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/dist/onlyne.d.ts DELETED
@@ -1,70 +0,0 @@
1
- import { type ChildProcess } from "node:child_process";
2
- import { type Socket } from "node:net";
3
- import type { Workspace } from "./workspace.js";
4
- export interface OnlyneRequest {
5
- id: string;
6
- op: string;
7
- op_id?: string;
8
- channel_id?: string;
9
- conversation_id?: string;
10
- message_id?: string;
11
- text?: string;
12
- format?: "plain" | "markdown";
13
- raw_text?: boolean;
14
- limit?: number;
15
- offset?: number;
16
- priority?: number;
17
- consume_timeout_ms?: number;
18
- event_seq?: number;
19
- }
20
- export interface SendTarget {
21
- channelId: string;
22
- }
23
- export interface SendResult extends SendTarget {
24
- ok: boolean;
25
- error?: string;
26
- }
27
- export declare function request(socketPath: string, req: OnlyneRequest): Promise<any>;
28
- export interface SwarmSubscription extends Socket {
29
- ack: Promise<any>;
30
- }
31
- export declare function subscribe(socketPath: string, onLine: (line: any) => void, onDisconnect?: () => void, opts?: {
32
- priority?: number;
33
- consumeTimeoutMs?: number;
34
- }): SwarmSubscription;
35
- export declare function waitForSocket(socketPath: string, timeoutMs?: number): Promise<void>;
36
- export declare function connectDaemon(ws: Workspace, startIfMissing?: boolean): Promise<{
37
- owner: "external" | "extension";
38
- process?: ChildProcess;
39
- }>;
40
- export declare function shutdownDaemon(ws: Workspace, child?: ChildProcess): Promise<void>;
41
- export declare function stopProcess(child?: ChildProcess): void;
42
- export declare function swarmReady(socketPath: string, workspace: string, terminalHandle: string, snapshot?: unknown, opId?: string): Promise<any>;
43
- export declare function swarmRecycled(socketPath: string, taskId: string, terminalHandle: string, reason: string, opId?: string): Promise<any>;
44
- /** R4 hop activity: turn started. Fire-and-forget; the scheduler treats a
45
- * missing task row as race residue and drops it silently. `body` comes from
46
- * swarm.ts hopActivityBody (the shared, unit-tested decision). */
47
- export declare function swarmBusy(socketPath: string, body: {
48
- workspace: string;
49
- terminal_handle: string;
50
- task_id: string;
51
- }, opId?: string): Promise<any>;
52
- /** R4 hop activity: turn ended with no out. `body.pending_exit` mirrors whether
53
- * the hop still awaits swarm_complete — the scheduler only records it. */
54
- export declare function swarmIdle(socketPath: string, body: {
55
- workspace: string;
56
- terminal_handle: string;
57
- task_id: string;
58
- pending_exit?: boolean;
59
- }, opId?: string): Promise<any>;
60
- export declare function consumeEvent(socket: Socket, eventSeq: number): Promise<void>;
61
- export declare function loopback(socketPath: string, text: string, rawText?: boolean, opId?: string): Promise<any>;
62
- /** Versioned lifecycle evidence for the scheduler reducer. The frame travels as
63
- * a raw-text loopback inbound: the daemon stores `text` verbatim and publishes
64
- * it on the same event stream the report consumer reads. `raw_text` keeps the
65
- * frame out of Markdown processing, and `opId` makes a socket-level retry
66
- * return the stored receipt. */
67
- export declare function sendSwarmReport(socketPath: string, frame: string, opId?: string): Promise<any>;
68
- export declare function markConsumed(socketPath: string, messageId: string): Promise<any>;
69
- export declare function sendWithRetry(socketPath: string, target: SendTarget, text: string, attempts: number, rawText?: boolean, opId?: string): Promise<SendResult>;
70
- export declare function broadcast(socketPath: string, targets: SendTarget[], text: string, attempts: number, concurrency: number, rawText?: boolean): Promise<SendResult[]>;
package/dist/onlyne.js DELETED
@@ -1,194 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createConnection } from "node:net";
3
- import { SWARM_REPORT_PREFIX } from "./swarm.js";
4
- export function request(socketPath, req) {
5
- return new Promise((resolve, reject) => {
6
- const socket = createConnection(socketPath);
7
- let data = "";
8
- socket.setEncoding("utf8");
9
- socket.on("error", reject);
10
- socket.on("connect", () => socket.write(`${JSON.stringify(req)}\n`));
11
- socket.on("data", (chunk) => { data += chunk; const idx = data.indexOf("\n"); if (idx >= 0) {
12
- socket.end();
13
- try {
14
- const response = JSON.parse(data.slice(0, idx));
15
- if (response && response.ok === false)
16
- reject(new Error(response.error?.message ?? JSON.stringify(response.error ?? response)));
17
- else
18
- resolve(response);
19
- }
20
- catch (e) {
21
- reject(e);
22
- }
23
- } });
24
- });
25
- }
26
- export function subscribe(socketPath, onLine, onDisconnect, opts) {
27
- const socket = createConnection(socketPath);
28
- let buf = "";
29
- let closed = false;
30
- socket.setEncoding("utf8");
31
- let resolveAck;
32
- let rejectAck;
33
- socket.ack = new Promise((resolve, reject) => { resolveAck = resolve; rejectAck = reject; });
34
- const disconnect = () => { if (closed)
35
- return; closed = true; rejectAck(new Error("onlyne subscription disconnected")); onDisconnect?.(); };
36
- socket.on("error", disconnect);
37
- socket.on("close", disconnect);
38
- socket.on("connect", () => { if (!socket.destroyed)
39
- socket.write(`${JSON.stringify({ id: "sub", op: "subscribe_events", ...(opts?.priority !== undefined ? { priority: opts.priority } : {}), ...(opts?.consumeTimeoutMs !== undefined ? { consume_timeout_ms: opts.consumeTimeoutMs } : {}) })}\n`, () => { }); });
40
- socket.on("data", (chunk) => { buf += chunk; for (;;) {
41
- const idx = buf.indexOf("\n");
42
- if (idx < 0)
43
- break;
44
- const raw = buf.slice(0, idx);
45
- buf = buf.slice(idx + 1);
46
- if (!raw.trim())
47
- continue;
48
- try {
49
- const line = JSON.parse(raw);
50
- if (line.id === "sub") {
51
- if (line.ok)
52
- resolveAck(line);
53
- else
54
- rejectAck(new Error(line.error?.message ?? "subscribe_events rejected"));
55
- }
56
- onLine(line);
57
- }
58
- catch { /* malformed event lines stay outside the lifecycle protocol */ }
59
- } });
60
- return socket;
61
- }
62
- export async function waitForSocket(socketPath, timeoutMs = 5000) {
63
- const deadline = Date.now() + timeoutMs;
64
- let last;
65
- while (Date.now() < deadline) {
66
- try {
67
- await request(socketPath, { id: "ping", op: "ping" });
68
- return;
69
- }
70
- catch (e) {
71
- last = e;
72
- }
73
- await new Promise((r) => setTimeout(r, 100));
74
- }
75
- throw last instanceof Error ? last : new Error("onlyne socket not ready");
76
- }
77
- function spawnManagedDaemon(ws) {
78
- const bin = process.env.ONLYNE_BIN || "onlyne";
79
- const script = `
80
- parent="$1"; shift
81
- "$@" &
82
- child=$!
83
- trap 'kill "$child" 2>/dev/null; wait "$child" 2>/dev/null' INT TERM HUP EXIT
84
- while kill -0 "$parent" 2>/dev/null; do
85
- kill -0 "$child" 2>/dev/null || { wait "$child"; exit $?; }
86
- sleep 1
87
- done
88
- kill "$child" 2>/dev/null
89
- wait "$child" 2>/dev/null
90
- `;
91
- return spawn("sh", ["-c", script, "onlyne-supervisor", String(process.pid), bin, "--workspace", ws.root, "run"], { stdio: "ignore" });
92
- }
93
- export async function connectDaemon(ws, startIfMissing = true) {
94
- try {
95
- await request(ws.socketPath, { id: "ping", op: "ping" });
96
- return { owner: "external" };
97
- }
98
- catch (e) {
99
- if (!startIfMissing)
100
- throw new Error(`onlyne daemon is not running for ${ws.root}; start it with /onlyne daemon start`, { cause: e });
101
- const child = spawnManagedDaemon(ws);
102
- try {
103
- await waitForSocket(ws.socketPath);
104
- // 自己 spawn 的 daemon 已退出:竞态中输给了其他启动方,socket 归对方所有,降级为 external(只订阅、不 shutdown)。
105
- if (child.exitCode !== null || child.signalCode !== null)
106
- return { owner: "external" };
107
- return { owner: "extension", process: child };
108
- }
109
- catch (err) {
110
- stopProcess(child);
111
- throw err;
112
- }
113
- }
114
- }
115
- export async function shutdownDaemon(ws, child) {
116
- try {
117
- await request(ws.socketPath, { id: `shutdown-${Date.now()}`, op: "shutdown" });
118
- }
119
- catch { /* may already be down */ }
120
- stopProcess(child);
121
- }
122
- export function stopProcess(child) { if (!child || child.killed)
123
- return; try {
124
- child.kill("SIGTERM");
125
- }
126
- catch { /* ignore */ } }
127
- export async function swarmReady(socketPath, workspace, terminalHandle, snapshot, opId) {
128
- return request(socketPath, { id: `swarm-ready-${Date.now()}`, op: "swarm_ready", op_id: opId, text: JSON.stringify({ protocol: 2, workspace, terminal_handle: terminalHandle, snapshot }) });
129
- }
130
- export async function swarmRecycled(socketPath, taskId, terminalHandle, reason, opId) {
131
- return request(socketPath, { id: `swarm-recycled-${Date.now()}`, op: "swarm_recycled", op_id: opId, text: JSON.stringify({ task_id: taskId, terminal_handle: terminalHandle, reason }) });
132
- }
133
- /** R4 hop activity: turn started. Fire-and-forget; the scheduler treats a
134
- * missing task row as race residue and drops it silently. `body` comes from
135
- * swarm.ts hopActivityBody (the shared, unit-tested decision). */
136
- export async function swarmBusy(socketPath, body, opId) {
137
- return request(socketPath, { id: `swarm-busy-${Date.now()}`, op: "swarm_busy", op_id: opId, text: JSON.stringify(body) });
138
- }
139
- /** R4 hop activity: turn ended with no out. `body.pending_exit` mirrors whether
140
- * the hop still awaits swarm_complete — the scheduler only records it. */
141
- export async function swarmIdle(socketPath, body, opId) {
142
- return request(socketPath, { id: `swarm-idle-${Date.now()}`, op: "swarm_idle", op_id: opId, text: JSON.stringify(body) });
143
- }
144
- export async function consumeEvent(socket, eventSeq) {
145
- return new Promise((resolve) => { try {
146
- socket.write(`${JSON.stringify({ id: `consume-${Date.now()}`, op: "consume", event_seq: eventSeq })}\n`, () => resolve());
147
- }
148
- catch {
149
- resolve();
150
- } });
151
- }
152
- export async function loopback(socketPath, text, rawText = true, opId) {
153
- return request(socketPath, { id: `loopback-${Date.now()}`, op: "loopback", op_id: opId, text, raw_text: rawText });
154
- }
155
- /** Versioned lifecycle evidence for the scheduler reducer. The frame travels as
156
- * a raw-text loopback inbound: the daemon stores `text` verbatim and publishes
157
- * it on the same event stream the report consumer reads. `raw_text` keeps the
158
- * frame out of Markdown processing, and `opId` makes a socket-level retry
159
- * return the stored receipt. */
160
- export async function sendSwarmReport(socketPath, frame, opId) {
161
- if (!frame.startsWith(SWARM_REPORT_PREFIX))
162
- throw new Error("swarm report frame needs the ---swarm-report header");
163
- return loopback(socketPath, frame, true, opId);
164
- }
165
- export async function markConsumed(socketPath, messageId) {
166
- return request(socketPath, { id: `consume-${Date.now()}`, op: "mark_io_consumed", message_id: messageId });
167
- }
168
- export async function sendWithRetry(socketPath, target, text, attempts, rawText = false, opId) {
169
- let error = "unknown error";
170
- for (let i = 0; i < Math.max(1, attempts); i++) {
171
- try {
172
- const res = await request(socketPath, { id: `send-${Date.now()}-${i}`, op: "send_message", op_id: opId, channel_id: target.channelId, text, raw_text: rawText });
173
- if (res.ok)
174
- return { ...target, ok: true };
175
- error = res.error?.message ?? JSON.stringify(res.error ?? res);
176
- }
177
- catch (e) {
178
- error = e instanceof Error ? e.message : String(e);
179
- }
180
- }
181
- return { ...target, ok: false, error };
182
- }
183
- export async function broadcast(socketPath, targets, text, attempts, concurrency, rawText = false) {
184
- const out = [];
185
- let next = 0;
186
- async function worker() { for (;;) {
187
- const i = next++;
188
- if (i >= targets.length)
189
- return;
190
- out[i] = await sendWithRetry(socketPath, targets[i], text, attempts, rawText);
191
- } }
192
- await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, targets.length)) }, worker));
193
- return out;
194
- }
package/dist/session.d.ts DELETED
@@ -1,82 +0,0 @@
1
- export declare const SWARM_STATE_ENTRY = "pi-onlyne:swarm-state";
2
- export declare const SWARM_PROTOCOL = 2;
3
- export type AgentState = "booting" | "ready" | "running" | "idle" | "gone";
4
- export type DeliveryState = "none" | "pending" | "retrying" | "accepted" | "exhausted";
5
- export type ResourceState = "detached" | "attached" | "closing" | "closed";
6
- export type Lifecycle = "created" | "working" | "idle" | "exited";
7
- export type RecoveryState = "idle_waiting" | "idle_fault" | "draining";
8
- export interface SwarmSnapshot {
9
- protocol: 2;
10
- task_id: string;
11
- generation: number;
12
- seq: number;
13
- agent: AgentState;
14
- delivery: DeliveryState;
15
- resource: ResourceState;
16
- lifecycle: Lifecycle;
17
- recovery: RecoveryState;
18
- pending_intents: string[];
19
- at: string;
20
- }
21
- export interface SwarmIntent {
22
- op_id: string;
23
- kind: string;
24
- payload: unknown;
25
- attempts: number;
26
- status: "pending" | "retrying" | "accepted" | "exhausted";
27
- receipt?: unknown;
28
- last_error?: string;
29
- }
30
- export interface SwarmStateData {
31
- protocol: 2;
32
- task_id: string;
33
- generation: number;
34
- seq: number;
35
- snapshot: SwarmSnapshot;
36
- intents: SwarmIntent[];
37
- }
38
- export interface SessionEntry {
39
- type?: string;
40
- customType?: string;
41
- data?: unknown;
42
- }
43
- export interface StateContext {
44
- appendEntry: (customType: string, data?: unknown) => void;
45
- sessionManager?: {
46
- getEntries(): readonly SessionEntry[];
47
- };
48
- }
49
- export declare function restoreSwarmState(ctx: StateContext, taskId: string): SwarmStateData | undefined;
50
- export declare class SwarmSessionState {
51
- readonly taskId: string;
52
- private append;
53
- private _generation;
54
- private _seq;
55
- private _snapshot;
56
- private _intents;
57
- private timers;
58
- private heartbeatTimer?;
59
- constructor(ctx: StateContext, taskId: string, restored?: SwarmStateData);
60
- startSession(): SwarmSnapshot;
61
- get generation(): number;
62
- get seq(): number;
63
- get intents(): SwarmIntent[];
64
- snapshot(): SwarmSnapshot;
65
- private makeSnapshot;
66
- private persist;
67
- transition(patch: Partial<SwarmSnapshot>, kind?: string): SwarmSnapshot;
68
- heartbeat(): SwarmSnapshot;
69
- recordIntent(kind: string, payload: unknown, opId: string): SwarmIntent;
70
- receipt(opId: string, receipt: unknown): boolean;
71
- retry(opId: string, send: (intent: SwarmIntent) => Promise<unknown>, maxAttempts?: number): Promise<SwarmIntent>;
72
- startHeartbeat(intervalMs?: number, run?: () => void): void;
73
- stopHeartbeat(): void;
74
- shutdown(): void;
75
- }
76
- export declare function compareSwarmVersion(a: {
77
- generation: number;
78
- seq: number;
79
- }, b: {
80
- generation: number;
81
- seq: number;
82
- }): "stale" | "duplicate" | "new";
package/dist/session.js DELETED
@@ -1,91 +0,0 @@
1
- export const SWARM_STATE_ENTRY = "pi-onlyne:swarm-state";
2
- export const SWARM_PROTOCOL = 2;
3
- function isObject(v) { return !!v && typeof v === "object" && !Array.isArray(v); }
4
- function validState(v) {
5
- if (!isObject(v) || v.protocol !== 2 || typeof v.task_id !== "string" || typeof v.generation !== "number" || typeof v.seq !== "number" || !isObject(v.snapshot) || !Array.isArray(v.intents))
6
- return false;
7
- return v.snapshot.protocol === 2 && v.snapshot.task_id === v.task_id && v.snapshot.generation === v.generation && typeof v.snapshot.seq === "number";
8
- }
9
- export function restoreSwarmState(ctx, taskId) {
10
- let found;
11
- for (const entry of ctx.sessionManager?.getEntries() ?? []) {
12
- if (entry.type !== "custom" || entry.customType !== SWARM_STATE_ENTRY)
13
- continue;
14
- if (!validState(entry.data))
15
- throw new Error("malformed swarm v2 state entry");
16
- if (entry.data.task_id !== taskId)
17
- continue;
18
- if (!found || entry.data.seq > found.seq || entry.data.generation > found.generation)
19
- found = entry.data;
20
- }
21
- return found;
22
- }
23
- export class SwarmSessionState {
24
- taskId;
25
- append;
26
- _generation = 0;
27
- _seq = 0;
28
- _snapshot;
29
- _intents = new Map();
30
- timers = new Set();
31
- heartbeatTimer;
32
- constructor(ctx, taskId, restored) {
33
- this.taskId = taskId;
34
- this.append = ctx.appendEntry;
35
- if (restored) {
36
- this._generation = restored.generation;
37
- this._seq = restored.seq;
38
- this._snapshot = structuredClone(restored.snapshot);
39
- for (const i of restored.intents)
40
- this._intents.set(i.op_id, structuredClone(i));
41
- }
42
- else
43
- this._snapshot = this.makeSnapshot();
44
- }
45
- startSession() { this._generation++; return this.persist("session_start", { generation: this._generation }); }
46
- get generation() { return this._generation; }
47
- get seq() { return this._seq; }
48
- get intents() { return [...this._intents.values()].map((x) => structuredClone(x)); }
49
- snapshot() { return structuredClone(this._snapshot); }
50
- makeSnapshot() { return { protocol: 2, task_id: this.taskId, generation: this._generation, seq: this._seq, agent: "booting", delivery: "none", resource: "detached", lifecycle: "created", recovery: "idle_waiting", pending_intents: [...this._intents.keys()], at: new Date().toISOString() }; }
51
- persist(kind, patch) { this._seq++; this._snapshot = { ...this._snapshot, ...patch, protocol: 2, task_id: this.taskId, generation: this._generation, seq: this._seq, pending_intents: [...this._intents.values()].filter((i) => i.status !== "accepted").map((i) => i.op_id), at: new Date().toISOString() }; this.append(SWARM_STATE_ENTRY, { protocol: 2, task_id: this.taskId, generation: this._generation, seq: this._seq, snapshot: this._snapshot, intents: this.intents, kind }); return this.snapshot(); }
52
- transition(patch, kind = "snapshot") { return this.persist(kind, patch); }
53
- heartbeat() { return this.persist("swarm_heartbeat"); }
54
- recordIntent(kind, payload, opId) { if (this._intents.has(opId))
55
- return structuredClone(this._intents.get(opId)); const intent = { op_id: opId, kind, payload, attempts: 0, status: "pending" }; this._intents.set(opId, intent); this.persist(kind, { delivery: "pending" }); return structuredClone(intent); }
56
- receipt(opId, receipt) { const i = this._intents.get(opId); if (!i)
57
- return false; i.status = "accepted"; i.receipt = receipt; this.persist("intent_receipt", { delivery: "accepted", lifecycle: i.kind === "swarm_complete" ? "exited" : this._snapshot.lifecycle }); return true; }
58
- async retry(opId, send, maxAttempts = 3) { const i = this._intents.get(opId); if (!i)
59
- throw new Error(`unknown intent ${opId}`); for (let n = i.attempts; n < maxAttempts && i.status !== "accepted"; n++) {
60
- i.attempts++;
61
- i.status = "retrying";
62
- this.persist("intent_attempt", { delivery: "retrying" });
63
- try {
64
- const receipt = await send(structuredClone(i));
65
- this.receipt(opId, receipt);
66
- }
67
- catch (e) {
68
- i.last_error = e instanceof Error ? e.message : String(e);
69
- if (i.attempts >= maxAttempts) {
70
- i.status = "exhausted";
71
- this.persist("intent_exhausted", { delivery: "exhausted", recovery: "idle_fault" });
72
- }
73
- else {
74
- const delay = 1000 * 2 ** (i.attempts - 1);
75
- await new Promise((resolve) => { const t = setTimeout(() => { this.timers.delete(t); resolve(); }, delay); this.timers.add(t); });
76
- }
77
- }
78
- } return structuredClone(i); }
79
- startHeartbeat(intervalMs = 10_000, run = () => this.heartbeat()) {
80
- this.stopHeartbeat();
81
- this.heartbeatTimer = setInterval(() => run(), Math.max(1, intervalMs));
82
- this.heartbeatTimer.unref?.();
83
- }
84
- stopHeartbeat() { if (this.heartbeatTimer)
85
- clearInterval(this.heartbeatTimer); this.heartbeatTimer = undefined; }
86
- shutdown() { this.stopHeartbeat(); for (const t of this.timers)
87
- clearTimeout(t); this.timers.clear(); }
88
- }
89
- export function compareSwarmVersion(a, b) { if (a.generation < b.generation || (a.generation === b.generation && a.seq < b.seq))
90
- return "stale"; if (a.generation === b.generation && a.seq === b.seq)
91
- return "duplicate"; return "new"; }
@@ -1,12 +0,0 @@
1
- export interface SwarmPromptConfig {
2
- enabled: boolean;
3
- template?: string;
4
- fallback?: string;
5
- }
6
- export declare function readSwarmPromptConfig(workspaceRoot: string): SwarmPromptConfig;
7
- export declare function resolveSwarmPrompt(workspaceRoot: string, header: {
8
- task_id: string;
9
- from: string;
10
- transfer_send_to: string;
11
- attempt: number;
12
- }, payload: string): string | null;
@@ -1,45 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- const DEFAULT_PROMPT = `Onlyne swarm task {task_id} (from {from}):
4
-
5
- {payload}
6
-
7
- This session carries this one task only. Restore context from files, work, spawn continuations with swarm_send when another unit must continue, then exit with swarm_complete. Downstream results travel through files and the ledger; nothing waits here.`;
8
- export function readSwarmPromptConfig(workspaceRoot) {
9
- const cfgPath = join(workspaceRoot, ".pi", "onlyne.json");
10
- if (!existsSync(cfgPath)) {
11
- return { enabled: true, fallback: DEFAULT_PROMPT };
12
- }
13
- try {
14
- const parsed = JSON.parse(readFileSync(cfgPath, "utf8"));
15
- const sw = parsed?.swarm_prompt;
16
- if (sw === false || (sw && sw.enabled === false))
17
- return { enabled: false, fallback: DEFAULT_PROMPT };
18
- return {
19
- enabled: sw?.enabled !== false,
20
- template: sw?.template,
21
- fallback: sw?.fallback ?? DEFAULT_PROMPT,
22
- };
23
- }
24
- catch {
25
- return { enabled: true, fallback: DEFAULT_PROMPT };
26
- }
27
- }
28
- export function resolveSwarmPrompt(workspaceRoot, header, payload) {
29
- const cfg = readSwarmPromptConfig(workspaceRoot);
30
- if (!cfg.enabled)
31
- return null;
32
- let tmpl = cfg.fallback ?? DEFAULT_PROMPT;
33
- if (cfg.template) {
34
- const abs = join(workspaceRoot, cfg.template);
35
- if (existsSync(abs)) {
36
- tmpl = readFileSync(abs, "utf8");
37
- }
38
- }
39
- return tmpl
40
- .replace(/{task_id}/g, header.task_id)
41
- .replace(/{from}/g, header.from)
42
- .replace(/{transfer_send_to}/g, header.transfer_send_to)
43
- .replace(/{attempt}/g, String(header.attempt))
44
- .replace(/{payload}/g, payload);
45
- }
@@ -1,43 +0,0 @@
1
- export type SwarmHandleResult = "claimed" | "yielded" | "not-swarm";
2
- export interface SwarmPi {
3
- sendUserMessage: (content: string, opts: {
4
- deliverAs: "followUp";
5
- }) => void;
6
- }
7
- /**
8
- * Single atomic task slot for swarm mode, extracted for unit testing.
9
- * index.ts delegates its state transitions here; the only coupling is the
10
- * followUp injection callback.
11
- */
12
- export declare class SwarmSlot {
13
- private taskId?;
14
- private from;
15
- private transfer;
16
- private attempt;
17
- private sentChildIds;
18
- handle(pi: SwarmPi, text: string, preferredTaskId?: string, workspaceRoot?: string): SwarmHandleResult;
19
- task(): {
20
- taskId?: string;
21
- from: string;
22
- transferSendTo: string;
23
- attempt: number;
24
- sentChildIds: string[];
25
- };
26
- noteSpawned(childId: string): void;
27
- taskIdOf(): string | undefined;
28
- clear(): void;
29
- }
30
- /** Test seam: fresh slot without touching module-global pi-onlyne state. */
31
- export declare function __swarmSlotForTest(): {
32
- handle: (pi: SwarmPi, text: string, preferredTaskId?: string, workspaceRoot?: string) => SwarmHandleResult;
33
- task: () => {
34
- taskId?: string;
35
- from: string;
36
- transferSendTo: string;
37
- attempt: number;
38
- sentChildIds: string[];
39
- };
40
- taskId: () => string | undefined;
41
- noteSpawned: (childId: string) => void;
42
- clear: () => void;
43
- };
@@ -1,76 +0,0 @@
1
- import { parseSwarmHeader } from "./swarm.js";
2
- import { resolveSwarmPrompt } from "./swarm-prompt.js";
3
- /**
4
- * Single atomic task slot for swarm mode, extracted for unit testing.
5
- * index.ts delegates its state transitions here; the only coupling is the
6
- * followUp injection callback.
7
- */
8
- export class SwarmSlot {
9
- taskId;
10
- from = ".";
11
- transfer = "";
12
- attempt = 1;
13
- sentChildIds = [];
14
- handle(pi, text, preferredTaskId, workspaceRoot) {
15
- const parsed = parseSwarmHeader(text);
16
- if (!parsed)
17
- return "not-swarm";
18
- const { header, delivery, payload } = parsed;
19
- // A scheduler-created terminal carries ONLYNE_SWARM_TASK. Its task id
20
- // is a hard delivery binding: history replay or a stale live event for
21
- // another task must never claim this session. Without this guard a
22
- // freshly spawned pane can run an expired workspace-local payload
23
- // before its assigned FIFO write reaches history.
24
- if (preferredTaskId && header.task_id !== preferredTaskId)
25
- return "not-swarm";
26
- // Only the scheduler's second delivery is claimable. A worker's raw
27
- // swarm_send relay and every out wire omit this structured field, so
28
- // history catchup cannot claim either one as an executable task.
29
- if (delivery !== "scheduler")
30
- return "not-swarm";
31
- if (!this.taskId) {
32
- this.taskId = header.task_id;
33
- this.from = header.from;
34
- this.transfer = header.transfer_send_to;
35
- this.attempt = header.attempt;
36
- this.sentChildIds = [];
37
- // A scheduler delivery is a real user task. `sendUserMessage` always
38
- // triggers a turn, including during a fresh Pi session; custom
39
- // `sendMessage` follow-ups can remain queued until a later event while
40
- // the session_start hook is still settling.
41
- pi.sendUserMessage(resolveSwarmPrompt(workspaceRoot ?? process.cwd(), header, payload) ?? payload, { deliverAs: "followUp" });
42
- return "claimed";
43
- }
44
- // A claimed session normally receives no further delivery. The
45
- // preferred-id guard above already protected an env-bound pane, so this
46
- // branch only applies to manual ready-pool sessions with no env task.
47
- return "not-swarm";
48
- }
49
- task() {
50
- return { taskId: this.taskId, from: this.from, transferSendTo: this.transfer, attempt: this.attempt, sentChildIds: [...this.sentChildIds] };
51
- }
52
- noteSpawned(childId) {
53
- this.sentChildIds.push(childId);
54
- }
55
- taskIdOf() {
56
- return this.taskId;
57
- }
58
- clear() {
59
- this.taskId = undefined;
60
- this.from = ".";
61
- this.transfer = "";
62
- this.attempt = 1;
63
- this.sentChildIds = [];
64
- }
65
- }
66
- /** Test seam: fresh slot without touching module-global pi-onlyne state. */
67
- export function __swarmSlotForTest() {
68
- const slot = new SwarmSlot();
69
- return {
70
- handle: (pi, text, preferredTaskId, workspaceRoot) => slot.handle(pi, text, preferredTaskId, workspaceRoot),
71
- task: () => slot.task(),
72
- taskId: () => slot.taskIdOf(),
73
- noteSpawned: (childId) => slot.noteSpawned(childId),
74
- clear: () => slot.clear(),
75
- };
76
- }