pi-orchestrator-config 4.2.0 → 4.3.2
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 +76 -67
- package/agents/code-reviewer-docs.md +11 -1
- package/agents/code-reviewer-guidelines.md +11 -1
- package/agents/code-reviewer-quality.md +11 -1
- package/agents/code-reviewer-security.md +11 -1
- package/agents/code-reviewer-spec.md +11 -1
- package/agents/git-expert.md +42 -3
- package/agents/github-expert.md +11 -0
- package/extensions/acpx-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/agents/claude.ts +3 -2
- package/extensions/cli-provider/agents/cursor.ts +3 -5
- package/extensions/cli-provider/agents/gemini.ts +3 -2
- package/extensions/cli-provider/discover.ts +7 -7
- package/extensions/cli-provider/index.ts +5 -0
- package/extensions/cli-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/session-reaper.ts +5 -7
- package/extensions/cli-provider/sessions.ts +4 -0
- package/extensions/cli-provider/shared/discover-cache.ts +17 -176
- package/extensions/coms/coms-p2p.ts +1719 -423
- package/extensions/coms/coms-shared.ts +261 -39
- package/extensions/coms/coms-wrapper.ts +100 -62
- package/extensions/coms/index.ts +8 -4
- package/extensions/coms/mixed-turn.ts +26 -0
- package/extensions/coms/probe-socket.ts +42 -0
- package/extensions/coms/themeMap.ts +0 -1
- package/extensions/image-gen/image-gen.ts +19 -1
- package/extensions/orchestrator/async-agents.ts +563 -102
- package/extensions/orchestrator/async-capability.ts +8 -8
- package/extensions/orchestrator/cron.ts +15 -9
- package/extensions/orchestrator/dreaming.ts +32 -25
- package/extensions/orchestrator/enforcement-helpers.ts +459 -17
- package/extensions/orchestrator/enforcement-rules.ts +5 -4
- package/extensions/orchestrator/enforcement.ts +80 -27
- package/extensions/orchestrator/extended-autocomplete.ts +7 -0
- package/extensions/orchestrator/git-helpers.ts +2 -2
- package/extensions/orchestrator/index.ts +78 -10
- package/extensions/orchestrator/markdown-transformer.ts +313 -0
- package/extensions/orchestrator/memory-embeddings.ts +36 -34
- package/extensions/orchestrator/memory-provenance.ts +29 -25
- package/extensions/orchestrator/memory-scoring.ts +23 -18
- package/extensions/orchestrator/memory-tools.ts +10 -3
- package/extensions/orchestrator/overlay-dashboard.ts +17 -5
- package/extensions/orchestrator/parse-model-override.ts +29 -0
- package/extensions/orchestrator/pi-config-review-state.ts +57 -33
- package/extensions/orchestrator/project-settings.ts +333 -127
- package/extensions/orchestrator/resolve-agent-model.ts +24 -13
- package/extensions/orchestrator/review-porcelain.ts +4 -0
- package/extensions/orchestrator/review-ui.ts +63 -15
- package/extensions/orchestrator/rule-placeholders.ts +411 -2
- package/extensions/orchestrator/rules.ts +80 -132
- package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
- package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
- package/extensions/orchestrator/settings-tui.ts +636 -0
- package/extensions/orchestrator/state-jsonl.ts +431 -0
- package/extensions/orchestrator/subagent-tool.ts +157 -59
- package/extensions/orchestrator/sync-limit.ts +23 -0
- package/extensions/orchestrator/task-lifecycle.ts +77 -67
- package/extensions/orchestrator/utils.ts +8 -5
- package/extensions/pidash/pidash.ts +193 -156
- package/extensions/pidiff/pidiff.ts +96 -38
- package/extensions/pidiff/spawn-lock.ts +62 -0
- package/extensions/pitasks/index.ts +430 -0
- package/extensions/pitasks/reminders.ts +37 -0
- package/extensions/pitasks/task-store.ts +467 -0
- package/extensions/pitasks/task-tools.ts +375 -0
- package/extensions/pitasks/task-widget.ts +181 -0
- package/extensions/pitasks/types.ts +38 -0
- package/extensions/providers/acpx-driver.ts +12 -2
- package/extensions/providers/built-in-drivers.ts +6 -6
- package/extensions/providers/claude-driver.ts +1 -1
- package/extensions/providers/cursor-cli-driver.ts +1 -1
- package/extensions/providers/gemini-driver.ts +1 -1
- package/extensions/providers/index.ts +144 -26
- package/extensions/providers/initialized-guard.ts +27 -0
- package/extensions/providers/restore-default-model.ts +596 -0
- package/extensions/providers/session-shutdown.ts +39 -0
- package/extensions/shared/coms-active.ts +23 -0
- package/extensions/shared/create-runtime-provider.ts +2 -0
- package/extensions/shared/daemon-manager.ts +0 -8
- package/extensions/shared/file-logger.ts +132 -25
- package/extensions/shared/logger.ts +50 -0
- package/extensions/shared/models-dev.ts +421 -0
- package/extensions/shared/oneshot.ts +116 -0
- package/extensions/shared/resolve-binary.ts +191 -0
- package/extensions/shared/types.ts +3 -0
- package/package.json +9 -3
- package/prompts/coderabbit-rate-limit.md +2 -0
- package/rules/00-orchestrator-core.md +3 -0
- package/rules/10-agent-routing.md +12 -1
- package/rules/20-code-review-loop.md +52 -25
- package/rules/30-prompt-templates.md +4 -0
- package/rules/35-memory.md +3 -3
- package/rules/40-critical-rules.md +3 -0
- package/rules/55-coms-protocol.md +84 -18
- package/rules/60-task-tracking.md +2 -0
- package/settings-keys.json +563 -0
- package/templates/coms-feature-manager-prompt.md +4 -6
- package/extensions/coms/coms-net-server.ts +0 -1642
- package/extensions/coms/coms-net-wrapper.ts +0 -521
- package/extensions/coms/coms-net.ts +0 -1589
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* coms-shared.ts — Shared utilities for coms
|
|
2
|
+
* coms-shared.ts — Shared utilities for coms wrapper.
|
|
3
3
|
*
|
|
4
4
|
* Handles reload resilience: when coms is active and the user runs /reload,
|
|
5
5
|
* session_shutdown fires (upstream cleans up the old socket/connection),
|
|
@@ -13,6 +13,10 @@ import * as net from "node:net";
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import * as crypto from "node:crypto";
|
|
15
15
|
import * as os from "node:os";
|
|
16
|
+
import { setComsActive } from "../shared/coms-active.js";
|
|
17
|
+
import { createLogger } from "../shared/logger.js";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("coms");
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* Tokenize a command string respecting double and single quotes.
|
|
@@ -73,7 +77,7 @@ export interface DeferredUpstream {
|
|
|
73
77
|
flagValues: Map<string, any>;
|
|
74
78
|
/** Whether the upstream extension is active */
|
|
75
79
|
active: boolean;
|
|
76
|
-
/** Extra persisted state
|
|
80
|
+
/** Extra persisted state */
|
|
77
81
|
extra?: Record<string, any>;
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -101,11 +105,33 @@ export function createDeferredProxy(
|
|
|
101
105
|
case 'getFlag':
|
|
102
106
|
return (name: string) => state.flagValues.get(name);
|
|
103
107
|
case 'registerCommand':
|
|
104
|
-
|
|
108
|
+
// Upstream /coms is owned by the wrapper — swallow it.
|
|
109
|
+
// Pass through utility commands (e.g. /coms-queue) so they
|
|
110
|
+
// are always available on the real pi, with an active-gate in the handler.
|
|
111
|
+
return (name: string, def: any) => {
|
|
112
|
+
if (name === 'coms') return;
|
|
113
|
+
const origHandler = def?.handler;
|
|
114
|
+
if (typeof origHandler === 'function') {
|
|
115
|
+
def.handler = async (args: string, ctx: any) => {
|
|
116
|
+
if (!state.active) {
|
|
117
|
+
try {
|
|
118
|
+
ctx.ui.notify(
|
|
119
|
+
name.includes('net')
|
|
120
|
+
? '📡 coms not active — no queue. Run /coms start first.'
|
|
121
|
+
: '📡 coms not active — no queue. Run /coms start first.',
|
|
122
|
+
'info',
|
|
123
|
+
);
|
|
124
|
+
} catch { /* ignore */ }
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
return origHandler(args, ctx);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return target.registerCommand(name, def);
|
|
131
|
+
};
|
|
105
132
|
case 'registerTool':
|
|
106
133
|
return (tool: any) => {
|
|
107
134
|
// Inject anti-loop warning into coms_send description
|
|
108
|
-
// (upstream coms_net_send has this but coms_send doesn't)
|
|
109
135
|
if (tool.name === "coms_send" && tool.description && !tool.description.includes("DO NOT")) {
|
|
110
136
|
tool.description +=
|
|
111
137
|
"\n\n\u26a0\ufe0f DO NOT call this tool to REPLY to an inbound message. " +
|
|
@@ -161,8 +187,14 @@ export function createDeferredProxy(
|
|
|
161
187
|
try {
|
|
162
188
|
await handler(evt, ctx);
|
|
163
189
|
state.active = true;
|
|
190
|
+
setComsActive(true);
|
|
164
191
|
} catch (err) {
|
|
165
|
-
|
|
192
|
+
state.active = false;
|
|
193
|
+
setComsActive(false);
|
|
194
|
+
persistState(target, persistKey, state);
|
|
195
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
196
|
+
const error = err instanceof Error ? err : new Error(msg);
|
|
197
|
+
log.error("reload reactivation failed", msg, error);
|
|
166
198
|
}
|
|
167
199
|
}
|
|
168
200
|
});
|
|
@@ -199,21 +231,21 @@ export function persistState(pi: ExtensionAPI, persistKey: string, state: Deferr
|
|
|
199
231
|
|
|
200
232
|
/**
|
|
201
233
|
* Prune stale coms registry entries on startup — removes entries with dead PIDs.
|
|
202
|
-
* Call from session_start in
|
|
234
|
+
* Call from session_start in coms.ts.
|
|
203
235
|
*/
|
|
204
236
|
let _pruned = false;
|
|
205
237
|
/**
|
|
206
238
|
* Prune stale coms registry entries — non-blocking.
|
|
207
|
-
* Runs once per session
|
|
239
|
+
* Runs once per session.
|
|
208
240
|
*/
|
|
209
|
-
export function pruneStaleRegistry(): void {
|
|
241
|
+
export function pruneStaleRegistry(comsDir?: string): void {
|
|
210
242
|
if (_pruned) return;
|
|
211
243
|
_pruned = true;
|
|
244
|
+
const resolvedComsDir = comsDir || path.join(os.homedir(), ".pi", "coms");
|
|
212
245
|
// Run async to avoid blocking session_start
|
|
213
246
|
setImmediate(() => {
|
|
214
247
|
try {
|
|
215
|
-
const
|
|
216
|
-
const projectsDir = path.join(comsDir, "projects");
|
|
248
|
+
const projectsDir = path.join(resolvedComsDir, "projects");
|
|
217
249
|
if (!fs.existsSync(projectsDir)) return;
|
|
218
250
|
let dirs: string[];
|
|
219
251
|
try { dirs = fs.readdirSync(projectsDir); } catch { return; }
|
|
@@ -280,7 +312,7 @@ export function pruneStaleRegistry(): void {
|
|
|
280
312
|
} catch { /* skip unreadable project directory */ }
|
|
281
313
|
}
|
|
282
314
|
// Cleanup orphan sockets (no matching registry entry)
|
|
283
|
-
const socketsDir = path.join(
|
|
315
|
+
const socketsDir = path.join(resolvedComsDir, "sockets");
|
|
284
316
|
if (fs.existsSync(socketsDir)) {
|
|
285
317
|
try {
|
|
286
318
|
const allEndpoints = new Set<string>();
|
|
@@ -316,7 +348,7 @@ export function pruneStaleRegistry(): void {
|
|
|
316
348
|
});
|
|
317
349
|
}
|
|
318
350
|
|
|
319
|
-
// ━━ Shared helpers (used by
|
|
351
|
+
// ━━ Shared helpers (used by coms-p2p.ts) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
320
352
|
|
|
321
353
|
export const FALLBACK_PALETTE = [
|
|
322
354
|
"#72F1B8", "#36F9F6", "#FF7EDB", "#FEDE5D",
|
|
@@ -429,40 +461,48 @@ export function readFrontmatterFromArgv(argv: string[]): { name?: string; descri
|
|
|
429
461
|
}
|
|
430
462
|
}
|
|
431
463
|
|
|
432
|
-
export function readTaskSummary(
|
|
464
|
+
export function readTaskSummary(_cwd: string, _sessionId?: string): { total: number; completed: number; in_progress: number } | null {
|
|
433
465
|
try {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (
|
|
442
|
-
try {
|
|
443
|
-
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
444
|
-
if (!Array.isArray(data.tasks) || data.tasks.length === 0) continue;
|
|
445
|
-
let total = 0, completed = 0, in_progress = 0;
|
|
446
|
-
for (const t of data.tasks) {
|
|
447
|
-
total++;
|
|
448
|
-
if (t.status === "completed") completed++;
|
|
449
|
-
else if (t.status === "in_progress") in_progress++;
|
|
450
|
-
}
|
|
451
|
-
return { total, completed, in_progress };
|
|
452
|
-
} catch { /* skip corrupt file */ }
|
|
466
|
+
const { listTasks } = require("../pitasks/index.js");
|
|
467
|
+
const tasks = listTasks();
|
|
468
|
+
if (!tasks || tasks.length === 0) return null;
|
|
469
|
+
let total = 0, completed = 0, in_progress = 0;
|
|
470
|
+
for (const t of tasks) {
|
|
471
|
+
total++;
|
|
472
|
+
if (t.status === "completed") completed++;
|
|
473
|
+
else if (t.status === "in_progress") in_progress++;
|
|
453
474
|
}
|
|
454
|
-
return
|
|
475
|
+
return { total, completed, in_progress };
|
|
455
476
|
} catch { return null; }
|
|
456
477
|
}
|
|
457
478
|
|
|
479
|
+
/** Build the customType string for coms inbound display (shown as the header by pi).
|
|
480
|
+
* Sanitizes all inputs since senderName/senderCwd come from untrusted peers. */
|
|
481
|
+
export function formatComsInboundType(senderName: string, selfName: string, senderCwd: string): string {
|
|
482
|
+
const safeSender = sanitizeComsName(senderName);
|
|
483
|
+
const safeSelf = sanitizeComsName(selfName);
|
|
484
|
+
const safeCwd = senderCwd.replace(/[\x00-\x1f\x7f-\x9f\[\]]/g, "").trim() || "?";
|
|
485
|
+
return `from ${safeSender} → ${safeSelf} @ ${safeCwd}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
458
488
|
export function buildInboundContent(
|
|
459
489
|
header: string,
|
|
460
490
|
prompt: string,
|
|
461
491
|
tasks?: Array<{ subject: string; description: string }> | null,
|
|
492
|
+
senderName?: string,
|
|
493
|
+
senderCwd?: string,
|
|
462
494
|
): string {
|
|
463
|
-
|
|
495
|
+
// Sanitize peer-controlled values — strip control chars and limit length
|
|
496
|
+
const safeName = senderName ? senderName.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 100) : undefined;
|
|
497
|
+
const safeCwd = senderCwd ? senderCwd.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200) : undefined;
|
|
498
|
+
// Machine-readable prefix so the LLM can distinguish coms messages from user input
|
|
499
|
+
const fromTag = safeName ? `[from ${safeName}${safeCwd ? ` @ ${safeCwd}` : ''}] ` : '';
|
|
500
|
+
let content = header ? `${fromTag}${header}\n\n${prompt}` : `${fromTag}${prompt}`;
|
|
501
|
+
if (safeName) {
|
|
502
|
+
content += `\n\nReply to me via coms_send(target="${safeName}")`;
|
|
503
|
+
}
|
|
464
504
|
if (tasks && tasks.length > 0) {
|
|
465
|
-
content += `\n\n## Assigned Tasks\n\
|
|
505
|
+
content += `\n\n## Assigned Tasks\n\nThese tasks have been added to your task list. Work through them in order:\n\n`;
|
|
466
506
|
for (const task of tasks) {
|
|
467
507
|
content += `- **${task.subject}**: ${task.description}\n`;
|
|
468
508
|
}
|
|
@@ -477,13 +517,47 @@ export interface TasksSummary {
|
|
|
477
517
|
}
|
|
478
518
|
|
|
479
519
|
/** Format the auto-delivered coms response text, including queued message IDs when > 0. */
|
|
480
|
-
|
|
520
|
+
/** Build the customType string for coms response display (shown as the header by pi). */
|
|
521
|
+
export function formatComsResponseType(targetName: string, selfName?: string, queuedMsgIds?: string[]): string {
|
|
522
|
+
const safeTarget = sanitizeComsName(targetName);
|
|
523
|
+
const arrow = selfName ? ` → ${sanitizeComsName(selfName)}` : "";
|
|
524
|
+
const ids = (queuedMsgIds ?? []).length <= 5
|
|
525
|
+
? (queuedMsgIds ?? []).join(", ")
|
|
526
|
+
: `${(queuedMsgIds ?? []).slice(0, 5).join(", ")} and ${(queuedMsgIds ?? []).length - 5} more`;
|
|
527
|
+
const queueNote = (queuedMsgIds ?? []).length > 0 ? ` (${(queuedMsgIds ?? []).length} more queued: ${ids})` : "";
|
|
528
|
+
return `coms response from ${safeTarget}${arrow}${queueNote}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Format the coms response content body (without the header — header is in customType). */
|
|
532
|
+
export function formatComsResponseBody(response: any, error: string | null): string {
|
|
533
|
+
if (error) return `Error: ${error}`;
|
|
534
|
+
if (typeof response === "string") return response;
|
|
535
|
+
if (response === undefined || response === null) return "(no response)";
|
|
536
|
+
return JSON.stringify(response, null, 2) ?? String(response);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Format the full auto-delivered coms response text, including queued message IDs.
|
|
541
|
+
* @deprecated Use formatComsResponseType + formatComsResponseBody instead for customType display.
|
|
542
|
+
*/
|
|
543
|
+
export function formatComsResponseText(targetName: string, response: any, error: string | null, queuedMsgIds: string[], selfName?: string): string {
|
|
481
544
|
const ids = queuedMsgIds.length <= 5 ? queuedMsgIds.join(", ") : `${queuedMsgIds.slice(0, 5).join(", ")} and ${queuedMsgIds.length - 5} more`;
|
|
482
545
|
const queueNote = queuedMsgIds.length > 0 ? ` (${queuedMsgIds.length} more queued: ${ids})` : "";
|
|
483
|
-
|
|
546
|
+
const arrow = selfName ? ` → ${selfName}` : "";
|
|
547
|
+
if (error) return `[coms response from ${targetName}${arrow}${queueNote}] Error: ${error}`;
|
|
484
548
|
return typeof response === "string"
|
|
485
|
-
? `[coms response from ${targetName}${queueNote}] ${response}`
|
|
486
|
-
: `[coms response from ${targetName}${queueNote}] ${JSON.stringify(response, null, 2)}`;
|
|
549
|
+
? `[coms response from ${targetName}${arrow}${queueNote}] ${response}`
|
|
550
|
+
: `[coms response from ${targetName}${arrow}${queueNote}] ${JSON.stringify(response, null, 2)}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Sanitize a coms identity name for safe embedding in header strings.
|
|
554
|
+
* Strips all control characters (C0/C1), brackets; replaces whitespace with hyphens. */
|
|
555
|
+
export function sanitizeComsName(name: string): string {
|
|
556
|
+
return name
|
|
557
|
+
.replace(/[\x00-\x1f\x7f-\x9f\[\]]/g, "") // strip C0/C1 control chars + brackets
|
|
558
|
+
.trim()
|
|
559
|
+
.replace(/\s+/g, "-") // whitespace → hyphens (keep as single token)
|
|
560
|
+
|| "?";
|
|
487
561
|
}
|
|
488
562
|
|
|
489
563
|
/** Return " 📨N" suffix string for queue depth, or "" when zero/absent. */
|
|
@@ -508,3 +582,151 @@ export function renderTasksPart(tasks: TasksSummary | null | undefined, theme: a
|
|
|
508
582
|
if (pending > 0) parts.push(theme.fg("dim", `${pending}◻`));
|
|
509
583
|
return theme.fg("dim", " ") + parts.join(theme.fg("dim", " "));
|
|
510
584
|
}
|
|
585
|
+
|
|
586
|
+
/** Minimal branch entry shape needed by detectMixedTurn. */
|
|
587
|
+
export interface BranchEntry {
|
|
588
|
+
type: string;
|
|
589
|
+
message?: { role: string };
|
|
590
|
+
customType?: string;
|
|
591
|
+
details?: { msg_id?: string };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Detect whether a user message appeared in the current turn alongside
|
|
596
|
+
* a coms inbound injection — indicating the assistant's response may be
|
|
597
|
+
* directed at the user, not the peer (#731).
|
|
598
|
+
*
|
|
599
|
+
* Walks the branch backwards: skips trailing assistant messages, then
|
|
600
|
+
* checks if a user message appears before reaching the specific coms
|
|
601
|
+
* inbound injection (identified by customType + msg_id).
|
|
602
|
+
*
|
|
603
|
+
* @param branch The session branch array (oldest → newest).
|
|
604
|
+
* @param inboundMsgId The msg_id of the currently-processing inbound.
|
|
605
|
+
* @param inboundCustomType The customType of the coms inbound injection
|
|
606
|
+
* ("coms-inbound" for p2p).
|
|
607
|
+
* @returns true if a user message was found between the last assistant
|
|
608
|
+
* message and the coms inbound injection (mixed turn).
|
|
609
|
+
*/
|
|
610
|
+
export function detectMixedTurn(
|
|
611
|
+
branch: ReadonlyArray<BranchEntry>,
|
|
612
|
+
inboundMsgId: string,
|
|
613
|
+
inboundCustomType: string,
|
|
614
|
+
): boolean {
|
|
615
|
+
let passedAssistant = false;
|
|
616
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
617
|
+
const entry = branch[i];
|
|
618
|
+
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
619
|
+
passedAssistant = true;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (!passedAssistant) continue; // still in trailing tool results etc.
|
|
623
|
+
if (entry.type === "custom_message" && entry.customType === inboundCustomType
|
|
624
|
+
&& entry.details?.msg_id === inboundMsgId) {
|
|
625
|
+
// Reached the coms inbound injection for this message — no user message in between.
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
if (entry.type === "message" && entry.message?.role === "user") {
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ── Task store integration (direct import from owned pitasks) ──────────
|
|
636
|
+
|
|
637
|
+
export interface ComsOrigin {
|
|
638
|
+
sender_session: string;
|
|
639
|
+
sender_name: string;
|
|
640
|
+
sender_endpoint?: string;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Max tasks accepted from a single inbound message to prevent abuse. */
|
|
644
|
+
const MAX_INBOUND_TASKS = 20;
|
|
645
|
+
/** Max length for task subject/description fields. */
|
|
646
|
+
const MAX_TASK_FIELD_LEN = 2000;
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Auto-create tasks on a peer's task store via owned TaskStore API.
|
|
650
|
+
* Runs on the SENDER side — uses createTaskForSession with the target's session ID.
|
|
651
|
+
* TaskStore handles locking, file format, everything.
|
|
652
|
+
*/
|
|
653
|
+
export function createComsInboundTasks(
|
|
654
|
+
tasks: Array<{ subject: string; description: string }>,
|
|
655
|
+
origin: ComsOrigin,
|
|
656
|
+
targetSessionId: string,
|
|
657
|
+
targetCwd?: string,
|
|
658
|
+
): number {
|
|
659
|
+
if (!tasks || tasks.length === 0 || !targetSessionId) return 0;
|
|
660
|
+
|
|
661
|
+
let createTaskForSession: any;
|
|
662
|
+
try {
|
|
663
|
+
createTaskForSession = require("../pitasks/index.js").createTaskForSession;
|
|
664
|
+
} catch { return 0; }
|
|
665
|
+
if (!createTaskForSession) return 0;
|
|
666
|
+
|
|
667
|
+
let created = 0;
|
|
668
|
+
const capped = tasks.slice(0, MAX_INBOUND_TASKS);
|
|
669
|
+
for (const task of capped) {
|
|
670
|
+
const subject = typeof task.subject === "string" ? task.subject.slice(0, MAX_TASK_FIELD_LEN) : "";
|
|
671
|
+
const description = typeof task.description === "string" ? task.description.slice(0, MAX_TASK_FIELD_LEN) : "";
|
|
672
|
+
if (!subject) continue;
|
|
673
|
+
try {
|
|
674
|
+
const createdBy = {
|
|
675
|
+
type: "coms" as const,
|
|
676
|
+
origin: origin.sender_name,
|
|
677
|
+
session: origin.sender_session,
|
|
678
|
+
project: "",
|
|
679
|
+
};
|
|
680
|
+
const meta = origin.sender_endpoint ? { sender_endpoint: origin.sender_endpoint } : undefined;
|
|
681
|
+
createTaskForSession(targetSessionId, subject, description, createdBy, meta, targetCwd);
|
|
682
|
+
created++;
|
|
683
|
+
} catch { /* best-effort */ }
|
|
684
|
+
}
|
|
685
|
+
return created;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Look up a task by ID and return it if created by a coms peer.
|
|
690
|
+
*/
|
|
691
|
+
export async function getComsOriginTask(
|
|
692
|
+
taskId: string,
|
|
693
|
+
): Promise<{ subject: string; createdBy: { session: string; name: string; endpoint?: string } } | null> {
|
|
694
|
+
if (!taskId || taskId === "-1") return null;
|
|
695
|
+
try {
|
|
696
|
+
const { getTask } = await import("../pitasks/index.js");
|
|
697
|
+
const task = getTask(taskId);
|
|
698
|
+
if (task?.createdBy?.type === "coms") {
|
|
699
|
+
return {
|
|
700
|
+
subject: task.subject,
|
|
701
|
+
createdBy: {
|
|
702
|
+
session: task.createdBy.session,
|
|
703
|
+
name: task.createdBy.origin,
|
|
704
|
+
endpoint: task.metadata?.sender_endpoint,
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
} catch { /* ignore */ }
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Get all tasks created by coms peers (for keepalive heartbeat).
|
|
714
|
+
*/
|
|
715
|
+
export async function getComsOriginTasks(): Promise<Array<{ taskId: string; subject: string; status: string; createdBy: { session: string; name: string; endpoint?: string } }>> {
|
|
716
|
+
try {
|
|
717
|
+
const { listTasks } = await import("../pitasks/index.js");
|
|
718
|
+
const tasks = listTasks();
|
|
719
|
+
return tasks
|
|
720
|
+
.filter((t: any) => t.createdBy?.type === "coms" && t.status !== "deleted")
|
|
721
|
+
.map((t: any) => ({
|
|
722
|
+
taskId: t.id,
|
|
723
|
+
subject: t.subject,
|
|
724
|
+
status: t.status,
|
|
725
|
+
createdBy: {
|
|
726
|
+
session: t.createdBy.session,
|
|
727
|
+
name: t.createdBy.origin,
|
|
728
|
+
endpoint: t.metadata?.sender_endpoint,
|
|
729
|
+
},
|
|
730
|
+
}));
|
|
731
|
+
} catch { return []; }
|
|
732
|
+
}
|
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
10
10
|
import { fuzzyFilter } from "@earendil-works/pi-tui";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import * as os from "node:os";
|
|
11
13
|
import { parseFlags, tokenizeArgs, createDeferredProxy, persistState, pruneStaleRegistry, type DeferredUpstream } from "./coms-shared.js";
|
|
12
14
|
import upstreamComsInit from "./coms-p2p.js";
|
|
15
|
+
import { getSetting } from "../orchestrator/project-settings.js";
|
|
16
|
+
import { setComsActive } from "../shared/coms-active.js";
|
|
17
|
+
import { createLogger } from "../shared/logger.js";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("coms");
|
|
13
20
|
|
|
14
21
|
function fuzzy(items: AutocompleteItem[], query: string): AutocompleteItem[] | null {
|
|
15
22
|
if (!query.trim()) return items.length > 0 ? items : null;
|
|
@@ -35,14 +42,94 @@ export function registerComs(pi: ExtensionAPI) {
|
|
|
35
42
|
|
|
36
43
|
// Prune stale registry entries on session start (cleans up after crashes)
|
|
37
44
|
// Also reset coms state on fresh starts (non-reload) to clear phantom peers
|
|
38
|
-
pi.on("session_start", (evt: any) => {
|
|
39
|
-
try {
|
|
45
|
+
pi.on("session_start", (evt: any, ctx: any) => {
|
|
46
|
+
try {
|
|
47
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
48
|
+
const comsDir = getSetting(cwd, "coms_dir") || path.join(os.homedir(), ".pi", "coms");
|
|
49
|
+
pruneStaleRegistry(comsDir);
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
const err = e instanceof Error ? e : new Error(String(e?.message ?? e));
|
|
52
|
+
log.warn("stale cleanup failed", err.message?.slice(0, 100), err);
|
|
53
|
+
}
|
|
40
54
|
if (evt?.reason !== "reload") {
|
|
41
55
|
state.active = false;
|
|
56
|
+
setComsActive(false);
|
|
42
57
|
persistState(pi, PERSIST_KEY, state);
|
|
43
58
|
}
|
|
44
59
|
});
|
|
45
60
|
|
|
61
|
+
const comsCmdHandler = async (args: string, ctx: any) => {
|
|
62
|
+
const trimmed = (args || "").trim();
|
|
63
|
+
const parts = tokenizeArgs(trimmed);
|
|
64
|
+
const subcommand = parts[0] || "status";
|
|
65
|
+
|
|
66
|
+
if (subcommand === "start") {
|
|
67
|
+
if (state.active) {
|
|
68
|
+
try { ctx.ui.notify("📡 coms already active", "warning"); } catch {}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
state.flagValues = new Map();
|
|
72
|
+
parseFlags(parts.slice(1), state.flagValues);
|
|
73
|
+
|
|
74
|
+
if (state.flagValues.has("name")) {
|
|
75
|
+
try { ctx.ui.notify("📡 coms: use --cname instead of --name", "error"); } catch {}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Default project to cwd so sessions in different dirs are isolated
|
|
80
|
+
if (!state.flagValues.has("project")) {
|
|
81
|
+
const cwd = ctx.cwd || "";
|
|
82
|
+
const proj = cwd.replace(/^[\\/]+/,"").replace(/[\\/]+/g, "__");
|
|
83
|
+
if (!proj) {
|
|
84
|
+
try { ctx.ui.notify("📡 coms: cannot start from /. Run from a project directory.", "error"); } catch {}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
state.flagValues.set("project", proj);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!state.capturedSessionStart) {
|
|
91
|
+
try { ctx.ui.notify("📡 coms: internal error — no session handler captured", "error"); } catch {}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await state.capturedSessionStart({}, ctx);
|
|
97
|
+
state.active = true;
|
|
98
|
+
setComsActive(true);
|
|
99
|
+
persistState(pi, PERSIST_KEY, state);
|
|
100
|
+
} catch (err: any) {
|
|
101
|
+
setComsActive(false);
|
|
102
|
+
const msg = err?.message ?? String(err);
|
|
103
|
+
const error = err instanceof Error ? err : new Error(msg);
|
|
104
|
+
log.error("/coms start failed", msg, error);
|
|
105
|
+
try { ctx.ui.notify(`📡 coms start failed: ${msg}`, "error"); } catch {}
|
|
106
|
+
}
|
|
107
|
+
} else if (subcommand === "stop") {
|
|
108
|
+
if (!state.active) {
|
|
109
|
+
try { ctx.ui.notify("📡 coms not active", "info"); } catch {}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (state.capturedSessionShutdown) {
|
|
113
|
+
try {
|
|
114
|
+
await state.capturedSessionShutdown();
|
|
115
|
+
} catch (err: any) {
|
|
116
|
+
const msg = err?.message ?? String(err);
|
|
117
|
+
const error = err instanceof Error ? err : new Error(msg);
|
|
118
|
+
log.warn("/coms stop: session shutdown threw", msg, error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
state.active = false;
|
|
122
|
+
setComsActive(false);
|
|
123
|
+
persistState(pi, PERSIST_KEY, state);
|
|
124
|
+
try { ctx.ui.notify("📡 coms stopped", "info"); } catch {}
|
|
125
|
+
} else if (subcommand === "status") {
|
|
126
|
+
try {
|
|
127
|
+
ctx.ui.notify(state.active ? "📡 coms: active (P2P)" : "📡 coms: inactive — run /coms start", "info");
|
|
128
|
+
} catch {}
|
|
129
|
+
} else {
|
|
130
|
+
try { ctx.ui.notify(`📡 coms: unknown subcommand "${subcommand}". Use: start | stop | status`, "warning"); } catch {}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
46
133
|
pi.registerCommand("coms", {
|
|
47
134
|
description: "P2P agent communication: /coms start | stop | status",
|
|
48
135
|
getArgumentCompletions: (prefix: string) => {
|
|
@@ -74,65 +161,16 @@ export function registerComs(pi: ExtensionAPI) {
|
|
|
74
161
|
}
|
|
75
162
|
return null;
|
|
76
163
|
},
|
|
77
|
-
handler:
|
|
78
|
-
const trimmed = (args || "").trim();
|
|
79
|
-
const parts = tokenizeArgs(trimmed);
|
|
80
|
-
const subcommand = parts[0] || "status";
|
|
81
|
-
|
|
82
|
-
if (subcommand === "start") {
|
|
83
|
-
if (state.active) {
|
|
84
|
-
try { ctx.ui.notify("📡 coms already active", "warning"); } catch {}
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
state.flagValues = new Map();
|
|
88
|
-
parseFlags(parts.slice(1), state.flagValues);
|
|
89
|
-
|
|
90
|
-
if (state.flagValues.has("name")) {
|
|
91
|
-
try { ctx.ui.notify("📡 coms: use --cname instead of --name", "error"); } catch {}
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Default project to cwd so sessions in different dirs are isolated
|
|
96
|
-
if (!state.flagValues.has("project")) {
|
|
97
|
-
const cwd = ctx.cwd || "";
|
|
98
|
-
const proj = cwd.replace(/^[\\/]/,"").replace(/[\\/]/g, "__");
|
|
99
|
-
if (!proj) {
|
|
100
|
-
try { ctx.ui.notify("📡 coms: cannot start from /. Run from a project directory.", "error"); } catch {}
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
state.flagValues.set("project", proj);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!state.capturedSessionStart) {
|
|
107
|
-
try { ctx.ui.notify("📡 coms: internal error — no session handler captured", "error"); } catch {}
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
await state.capturedSessionStart({}, ctx);
|
|
113
|
-
state.active = true;
|
|
114
|
-
persistState(pi, PERSIST_KEY, state);
|
|
115
|
-
} catch (err: any) {
|
|
116
|
-
try { ctx.ui.notify(`📡 coms start failed: ${err?.message ?? String(err)}`, "error"); } catch {}
|
|
117
|
-
}
|
|
118
|
-
} else if (subcommand === "stop") {
|
|
119
|
-
if (!state.active) {
|
|
120
|
-
try { ctx.ui.notify("📡 coms not active", "info"); } catch {}
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (state.capturedSessionShutdown) {
|
|
124
|
-
try { await state.capturedSessionShutdown(); } catch {}
|
|
125
|
-
}
|
|
126
|
-
state.active = false;
|
|
127
|
-
persistState(pi, PERSIST_KEY, state);
|
|
128
|
-
try { ctx.ui.notify("📡 coms stopped", "info"); } catch {}
|
|
129
|
-
} else if (subcommand === "status") {
|
|
130
|
-
try {
|
|
131
|
-
ctx.ui.notify(state.active ? "📡 coms: active (P2P)" : "📡 coms: inactive — run /coms start", "info");
|
|
132
|
-
} catch {}
|
|
133
|
-
} else {
|
|
134
|
-
try { ctx.ui.notify(`📡 coms: unknown subcommand "${subcommand}". Use: start | stop | status`, "warning"); } catch {}
|
|
135
|
-
}
|
|
136
|
-
},
|
|
164
|
+
handler: comsCmdHandler,
|
|
137
165
|
});
|
|
166
|
+
// Expose handler to pidash for browser command dispatch
|
|
167
|
+
try {
|
|
168
|
+
if ((globalThis as any).__coms_pidash_listener) {
|
|
169
|
+
try { pi.events.removeListener("pidash:request-commands", (globalThis as any).__coms_pidash_listener); } catch {}
|
|
170
|
+
}
|
|
171
|
+
const registerWithPidash = () => pi.events.emit("pidash:register-command", { name: "coms", handler: comsCmdHandler });
|
|
172
|
+
(globalThis as any).__coms_pidash_listener = registerWithPidash;
|
|
173
|
+
registerWithPidash();
|
|
174
|
+
pi.events.on("pidash:request-commands", registerWithPidash);
|
|
175
|
+
} catch {}
|
|
138
176
|
}
|
package/extensions/coms/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Coms Extension for pi
|
|
3
3
|
*
|
|
4
|
-
* Inter-agent communication — P2P (coms)
|
|
5
|
-
* Activated on-demand via /coms
|
|
4
|
+
* Inter-agent communication — P2P (coms).
|
|
5
|
+
* Activated on-demand via /coms slash command.
|
|
6
6
|
*
|
|
7
7
|
* Standalone extension so it can be loaded independently by pi-sidecar
|
|
8
8
|
* without pulling in the full orchestrator.
|
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { createLogger } from "../shared/logger.js";
|
|
15
|
+
import { shouldSkipOneshotRegister } from "../shared/oneshot.js";
|
|
14
16
|
import { registerComs } from "./coms-wrapper.js";
|
|
15
|
-
|
|
17
|
+
|
|
18
|
+
const log = createLogger("coms");
|
|
16
19
|
|
|
17
20
|
export default function (pi: ExtensionAPI) {
|
|
21
|
+
if (shouldSkipOneshotRegister(log)) return;
|
|
22
|
+
log.debug("register coms");
|
|
18
23
|
registerComs(pi);
|
|
19
|
-
registerComsNet(pi);
|
|
20
24
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extensions/coms/mixed-turn.ts — Pure helpers for coms mixed-turn detection.
|
|
3
|
+
* Kept dependency-free so both the coms extension handlers and unit tests can
|
|
4
|
+
* import the SAME production logic (no mirrored copies). The shared logger is
|
|
5
|
+
* dependency-light (node builtins only via file-logger) so importing it here
|
|
6
|
+
* does not pull in pi-tui and keeps the module test-importable — the same
|
|
7
|
+
* pattern used by extensions/pitasks/reminders.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createLogger } from "../shared/logger.js";
|
|
11
|
+
|
|
12
|
+
const log = createLogger("coms-mixed-turn");
|
|
13
|
+
|
|
14
|
+
/** True when a real user message (role "user") arrives while a coms inbound turn is being processed (#741). */
|
|
15
|
+
export function isUserMessageDuringInbound(processingInbound: boolean, role: string | undefined): boolean {
|
|
16
|
+
const result = processingInbound === true && role === "user";
|
|
17
|
+
log.debug("isUserMessageDuringInbound", processingInbound, role, result);
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** True when the turn must be handled as a mixed turn (re-inject the inbound instead of auto-capturing assistant text). */
|
|
22
|
+
export function computeMixedTurn(inboundSetDuringUserTurn: boolean, userMessageDuringInbound: boolean): boolean {
|
|
23
|
+
const result = inboundSetDuringUserTurn || userMessageDuringInbound;
|
|
24
|
+
log.debug("computeMixedTurn", inboundSetDuringUserTurn, userMessageDuringInbound, result);
|
|
25
|
+
return result;
|
|
26
|
+
}
|