oh-pi 0.1.76 → 0.1.77

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 CHANGED
@@ -157,7 +157,7 @@ Use `/colony-stop` to abort a running colony.
157
157
 
158
158
  ### Signal Protocol
159
159
 
160
- The colony communicates with the main conversation via structured signals, so the model never has to guess background state:
160
+ The colony communicates with the main conversation via structured signals, so the model never has to guess background state. Updates are passively pushed (non-blocking), so polling is optional:
161
161
 
162
162
  | Signal | Meaning |
163
163
  |--------|---------|
package/README.zh.md CHANGED
@@ -163,7 +163,7 @@ pi(主进程)
163
163
 
164
164
  ### 信号协议
165
165
 
166
- 蚁群通过结构化信号与主对话通信,让模型无需猜测后台状态:
166
+ 蚁群通过结构化信号与主对话通信,让模型无需猜测后台状态。更新采用被动推送(非阻塞),轮询仅在手动排障时需要:
167
167
 
168
168
  | 信号 | 含义 |
169
169
  |------|------|
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ async function quickFlow(env) {
43
43
  providers,
44
44
  theme: "dark",
45
45
  keybindings: "default",
46
- extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update", "smart-compact"],
46
+ extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
47
47
  prompts: ["review", "fix", "explain", "commit", "test"],
48
48
  agents: "general-developer",
49
49
  thinking: "medium",
package/dist/registry.js CHANGED
@@ -49,7 +49,6 @@ export const EXTENSIONS = [
49
49
  { name: "compact-header", label: "⚡ Compact Header — Dense startup info replacing verbose output", default: true },
50
50
  { name: "ant-colony", label: "🐜 Ant Colony — Autonomous multi-agent swarm with adaptive concurrency", default: false },
51
51
  { name: "auto-update", label: "🔄 Auto Update — Check for oh-pi updates on startup and notify", default: true },
52
- { name: "smart-compact", label: "🗜️ Smart Compact — Trim large tool outputs and old messages in-flight", default: true },
53
52
  { name: "bg-process", label: "⏳ Bg Process — Auto-background long-running commands (dev servers, etc.)", default: false },
54
53
  ];
55
54
  /** 快捷键绑定方案(default / vim / emacs) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.76",
3
+ "version": "0.1.77",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,8 +17,9 @@ You command an autonomous ant colony. Complex tasks are delegated to the swarm,
17
17
  ## Workflow
18
18
  1. Assess task scope
19
19
  2. If colony-worthy → use `ant_colony` tool with clear goal
20
- 3. If simple do it directly
21
- 4. Review colony output, fix gaps manually if needed
20
+ 3. After launch, use passive mode: wait for `COLONY_SIGNAL:*` updates; do not poll `bg_colony_status` unless user explicitly asks
21
+ 4. If simple do it directly
22
+ 5. Review colony output, fix gaps manually if needed
22
23
 
23
24
  ## Code Standards
24
25
  - Follow existing conventions
@@ -59,6 +59,44 @@ export default function antColonyExtension(pi: ExtensionAPI) {
59
59
  // 当前运行中的后台蚁群(同时只允许一个)
60
60
  let activeColony: BackgroundColony | null = null;
61
61
 
62
+ // 防止主进程主动轮询导致阻塞:仅允许显式请求的手动快照,并加冷却
63
+ let lastBgStatusSnapshotAt = 0;
64
+ const STATUS_SNAPSHOT_COOLDOWN_MS = 15_000;
65
+
66
+ const extractMessageText = (message: any): string => {
67
+ const c = message?.content;
68
+ if (typeof c === "string") return c;
69
+ if (Array.isArray(c)) {
70
+ return c.map((p: any) => {
71
+ if (typeof p === "string") return p;
72
+ if (typeof p?.text === "string") return p.text;
73
+ if (typeof p?.content === "string") return p.content;
74
+ return "";
75
+ }).join("\n");
76
+ }
77
+ return "";
78
+ };
79
+
80
+ const lastUserMessageText = (ctx: any): string => {
81
+ try {
82
+ const branch = ctx?.sessionManager?.getBranch?.() ?? [];
83
+ for (let i = branch.length - 1; i >= 0; i--) {
84
+ const e = branch[i];
85
+ if (e?.type === "message" && e.message?.role === "user") {
86
+ return extractMessageText(e.message).trim();
87
+ }
88
+ }
89
+ } catch {
90
+ // ignore
91
+ }
92
+ return "";
93
+ };
94
+
95
+ const isExplicitStatusRequest = (ctx: any): boolean => {
96
+ const text = lastUserMessageText(ctx);
97
+ return /(?:\/colony-status|bg_colony_status)|(?:(?:蚁群|colony).{0,20}(?:状态|进度|进展|汇报|快照|status|progress|snapshot|update|check))|(?:(?:状态|进度|进展|汇报|快照|status|progress|snapshot|update|check).{0,20}(?:蚁群|colony))/i.test(text);
98
+ };
99
+
62
100
  const calcProgress = (m?: ColonyMetrics | null) => {
63
101
  if (!m || m.tasksTotal <= 0) return 0;
64
102
  return Math.max(0, Math.min(1, m.tasksDone / m.tasksTotal));
@@ -280,6 +318,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
280
318
  colony.promise = resume ? resumeColony(colonyOpts) : runColony(colonyOpts);
281
319
 
282
320
  activeColony = colony;
321
+ lastBgStatusSnapshotAt = 0;
283
322
  throttledRender();
284
323
 
285
324
  // 后台等待完成,注入结果
@@ -627,7 +666,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
627
666
  launchBackgroundColony(colonyParams);
628
667
 
629
668
  return {
630
- content: [{ type: "text", text: `[COLONY_SIGNAL:LAUNCHED]\n🐜 Colony launched in background.\nGoal: ${params.goal}\n\nThe colony is now running autonomously. Results will be injected when it finishes.` }],
669
+ content: [{ type: "text", text: `[COLONY_SIGNAL:LAUNCHED]\n🐜 Colony launched in background.\nGoal: ${params.goal}\n\nThe colony runs autonomously in passive mode. Progress is pushed via [COLONY_SIGNAL:*] follow-up messages. Do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.` }],
631
670
  };
632
671
  },
633
672
 
@@ -689,9 +728,40 @@ export default function antColonyExtension(pi: ExtensionAPI) {
689
728
  pi.registerTool({
690
729
  name: "bg_colony_status",
691
730
  label: "Colony Status",
692
- description: "Check the status of a running background ant colony. Use this instead of bg_status to monitor colony progress.",
731
+ description: "Optional manual snapshot for a running colony. Progress is pushed passively via COLONY_SIGNAL follow-up messages; call this only when the user explicitly asks.",
693
732
  parameters: Type.Object({}),
694
- async execute() {
733
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
734
+ if (!activeColony) {
735
+ return {
736
+ content: [{ type: "text" as const, text: "No colony is currently running." }],
737
+ };
738
+ }
739
+
740
+ const explicit = isExplicitStatusRequest(ctx);
741
+ if (!explicit) {
742
+ return {
743
+ content: [{
744
+ type: "text" as const,
745
+ text: "Passive mode is active. Colony progress is already pushed via [COLONY_SIGNAL:*] follow-up messages. Skipping bg_colony_status polling to avoid blocking the main process. Ask explicitly for a manual snapshot if needed.",
746
+ }],
747
+ isError: true,
748
+ };
749
+ }
750
+
751
+ const now = Date.now();
752
+ const delta = now - lastBgStatusSnapshotAt;
753
+ if (delta < STATUS_SNAPSHOT_COOLDOWN_MS) {
754
+ const waitSec = Math.ceil((STATUS_SNAPSHOT_COOLDOWN_MS - delta) / 1000);
755
+ return {
756
+ content: [{
757
+ type: "text" as const,
758
+ text: `Manual status snapshot is rate-limited. Please wait ${waitSec}s to avoid active polling loops.`,
759
+ }],
760
+ isError: true,
761
+ };
762
+ }
763
+
764
+ lastBgStatusSnapshotAt = now;
695
765
  return {
696
766
  content: [{ type: "text" as const, text: buildStatusText() }],
697
767
  };
@@ -1,64 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { truncateText, compactContent } from "./smart-compact";
3
-
4
- const longMultiline = (lines: number, lineLen = 100) =>
5
- Array.from({ length: lines }, (_, i) => "x".repeat(lineLen) + i).join("\n");
6
-
7
- describe("truncateText", () => {
8
- it("short text returns unchanged", () => {
9
- expect(truncateText("hello", 8000)).toBe("hello");
10
- });
11
-
12
- it("few lines but long chars returns unchanged", () => {
13
- const text = "x".repeat(9000) + "\n" + "y".repeat(9000);
14
- expect(truncateText(text, 8000)).toBe(text);
15
- });
16
-
17
- it("long text with many lines gets truncated", () => {
18
- const text = longMultiline(200);
19
- const result = truncateText(text, 8000);
20
- expect(result).toContain("[...truncated");
21
- });
22
-
23
- it("preserves head and tail", () => {
24
- const text = longMultiline(200);
25
- const result = truncateText(text, 8000);
26
- expect(result.startsWith(text.slice(0, 1500))).toBe(true);
27
- expect(result.endsWith(text.slice(-2500))).toBe(true);
28
- });
29
-
30
- it("custom head/tail params work", () => {
31
- const text = longMultiline(200);
32
- const result = truncateText(text, 100, 50, 50);
33
- expect(result).toContain("[...truncated");
34
- expect(result.startsWith(text.slice(0, 50))).toBe(true);
35
- expect(result.endsWith(text.slice(-50))).toBe(true);
36
- });
37
- });
38
-
39
- describe("compactContent", () => {
40
- it("short string returns unchanged", () => {
41
- expect(compactContent("short")).toBe("short");
42
- });
43
-
44
- it("long string gets truncated", () => {
45
- const text = longMultiline(200);
46
- expect(compactContent(text)).toContain("[...truncated");
47
- });
48
-
49
- it("array text block gets truncated", () => {
50
- const text = longMultiline(200);
51
- const result = compactContent([{ type: "text", text }]);
52
- expect(result[0].text).toContain("[...truncated");
53
- });
54
-
55
- it("array non-text block returned unchanged", () => {
56
- const block = { type: "image", url: "x" };
57
- expect(compactContent([block])).toEqual([block]);
58
- });
59
-
60
- it("non-array non-string returned as-is", () => {
61
- expect(compactContent(42)).toBe(42);
62
- expect(compactContent({ a: 1 })).toEqual({ a: 1 });
63
- });
64
- });
@@ -1,89 +0,0 @@
1
- /**
2
- * 智能压缩扩展 — 在发送给 LLM 前裁剪大块内容
3
- *
4
- * 策略:
5
- * 1. 工具输出超过阈值 → 保留首尾,中间替换为 "[...truncated N lines]"
6
- * 2. 用户粘贴的大块文本 → 同上
7
- * 3. 越旧的消息裁剪越激进
8
- */
9
-
10
- const MAX_TOOL_OUTPUT_CHARS = 8000;
11
- const MAX_USER_BLOCK_CHARS = 12000;
12
- const KEEP_HEAD = 1500;
13
- const KEEP_TAIL = 2500;
14
-
15
- export function truncateText(text: string, max: number, head = KEEP_HEAD, tail = KEEP_TAIL): string {
16
- if (text.length <= max) return text;
17
- const lines = text.split("\n");
18
- if (lines.length <= 10) return text; // 短文本不裁
19
- const headText = text.slice(0, head);
20
- const tailText = text.slice(-tail);
21
- const removedLines = text.slice(head, -tail).split("\n").length;
22
- return `${headText}\n\n[...truncated ${removedLines} lines...]\n\n${tailText}`;
23
- }
24
-
25
- export function compactContent(content: any): any {
26
- if (typeof content === "string") {
27
- return truncateText(content, MAX_TOOL_OUTPUT_CHARS);
28
- }
29
- if (!Array.isArray(content)) return content;
30
- return content.map((block: any) => {
31
- if (block.type === "text" && typeof block.text === "string") {
32
- return { ...block, text: truncateText(block.text, MAX_TOOL_OUTPUT_CHARS) };
33
- }
34
- return block;
35
- });
36
- }
37
-
38
- export default function smartCompact(pi: any) {
39
- pi.on("context", async (event: any) => {
40
- const messages = event.messages;
41
- if (!messages || messages.length < 4) return; // 太短不处理
42
-
43
- // 只处理非最近 3 条消息(保留最近上下文完整)
44
- const cutoff = messages.length - 3;
45
-
46
- for (let i = 0; i < cutoff; i++) {
47
- const msg = messages[i];
48
- if (!msg) continue;
49
-
50
- if (msg.role === "toolResult") {
51
- msg.content = compactContent(msg.content);
52
- } else if (msg.role === "user") {
53
- // 用户消息用更宽松的阈值
54
- if (typeof msg.content === "string" && msg.content.length > MAX_USER_BLOCK_CHARS) {
55
- msg.content = truncateText(msg.content, MAX_USER_BLOCK_CHARS);
56
- } else if (Array.isArray(msg.content)) {
57
- msg.content = msg.content.map((block: any) => {
58
- if (block.type === "text" && typeof block.text === "string" && block.text.length > MAX_USER_BLOCK_CHARS) {
59
- return { ...block, text: truncateText(block.text, MAX_USER_BLOCK_CHARS) };
60
- }
61
- return block;
62
- });
63
- }
64
- } else if (msg.role === "assistant" && Array.isArray(msg.content)) {
65
- // 裁剪 assistant 的大块工具调用参数
66
- msg.content = msg.content.map((block: any) => {
67
- if (block.type === "toolCall" && block.arguments) {
68
- const args = JSON.stringify(block.arguments);
69
- if (args.length > MAX_TOOL_OUTPUT_CHARS) {
70
- try {
71
- const parsed = typeof block.arguments === "string" ? JSON.parse(block.arguments) : block.arguments;
72
- // 裁剪大字符串参数
73
- for (const key of Object.keys(parsed)) {
74
- if (typeof parsed[key] === "string" && parsed[key].length > MAX_TOOL_OUTPUT_CHARS) {
75
- parsed[key] = truncateText(parsed[key], MAX_TOOL_OUTPUT_CHARS);
76
- }
77
- }
78
- return { ...block, arguments: parsed };
79
- } catch { return block; }
80
- }
81
- }
82
- return block;
83
- });
84
- }
85
- }
86
-
87
- return { messages };
88
- });
89
- }