pi-legion 2.0.2 → 2.0.3

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/CHANGELOG.md CHANGED
@@ -11,6 +11,18 @@ summary so you can see **2.x vs 0.16** without leaving the tree.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [2.0.3] — 2026-10-15
15
+
16
+ ### Fixed
17
+
18
+ - `maxChildrenPerNode` only counts **live/running** children; `retire` (including
19
+ Already-retired no-op) frees the budget slot so a full tree can launch again
20
+ after retire. Session keep for resume does **not** occupy the concurrent slot
21
+ (#142 / #143).
22
+ - Header tok/s uses `MIN_RATE_GENERATION_MS=1000` (same floor as tree rows): when
23
+ the in-window generation delta is under 1s, fall back to wall-clock so tiny
24
+ `dGen` cannot explode rates into the millions (#140 / #141).
25
+
14
26
  ## [2.0.2] — 2026-10-16
15
27
 
16
28
  ### Fixed
@@ -131,6 +143,9 @@ branch — pin `pi-legion@0.16.x` if you still need that model. See
131
143
  Earlier 0.11–0.15 history (including the `pi-herdr-swarm` /
132
144
  `@zzjcool/pi-herdr-subagents` renames) lives in git tags and older Releases.
133
145
 
134
- [Unreleased]: https://github.com/zzjcool/pi-legion/compare/v2.0.0...HEAD
146
+ [Unreleased]: https://github.com/zzjcool/pi-legion/compare/v2.0.3...HEAD
147
+ [2.0.3]: https://github.com/zzjcool/pi-legion/compare/v2.0.2...v2.0.3
148
+ [2.0.2]: https://github.com/zzjcool/pi-legion/compare/v2.0.1...v2.0.2
149
+ [2.0.1]: https://github.com/zzjcool/pi-legion/compare/v2.0.0...v2.0.1
135
150
  [2.0.0]: https://github.com/zzjcool/pi-legion/releases/tag/v2.0.0
136
151
  [0.16.x]: https://github.com/zzjcool/pi-legion/releases
@@ -359,7 +359,7 @@ interface LegionEventBus {
359
359
  | 项 | 默认 | 判定 |
360
360
  |---|---|---|
361
361
  | `maxDepth`(全树深度) | 4 | 硬顶 = `MAX_NESTED_PATH_ENTRIES`(血缘路径在 4 处饱和;settings/env 超 4 截断——告警在 M1-integration 的 launch 路径接入,M0 阶段截断静默可接受)。每进程生效上限 C:根 `C = min(env 运营者覆盖, settings.legion.maxDepth, 4)`;深度 d、上限 C 的父 launch 角色 R:先过闸门 `d+1 > C` → BUDGET_EXCEEDED;再经 `PI_SUBAGENT_MAX_DEPTH` 下发 `C子 = min(C, d + R, 4)`(R 缺省=不收紧;非法值按叶子 R=1 处理;语义 = 子树代数含自身,位置无关,只收紧不放宽)。不变量:C子 ≤ C父,且每进程 depth ≤ maxDepth |
362
- | `maxChildrenPerNode`(单节点扇出) | 8 | 父 supervisor 查自己子节点数 |
362
+ | `maxChildrenPerNode`(单节点扇出) | 8 | 父 supervisor 查自己 **live** 子节点数(`starting`/`running`/`blocked`;`settled`/`failed`/`retired` 不占槽,session 可保留 resume) |
363
363
  | `maxActiveNodes`(全树并发 running+starting) | 30 | **全树真闸门**(替代只当 hint 的 maxConcurrentAgents):launch 前一条 SQL,超限 BUDGET_EXCEEDED + `budget_refused` 事件 |
364
364
 
365
365
  ### 11.2 心跳与孤儿:postman 循环每次刷新直接子节点 `updated_at`。根 supervisor 每 60s 扫描:非终态 + `updated_at` 超 90s = `orphan_detected` 事件 + mail 通知 CEO。恢复 = resume 链(CEO 裁决,auto-resume 默认关)。孤儿判定可判定化:**非终态 ∧ 无活跃进程 ∧ 心跳超时**。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-legion",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Pi Legion v2: recursive centurion trees on headless Pi RPC; shared SQLite WAL legion.db. Four-stage planning/implementing/reviewing/verifying state machine, parent/child/sibling legion_mail, three-layer /legion UI, upward MR trees, squad resume.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,5 @@
1
1
  import type { DatabaseSync } from "./db.ts";
2
+ import { LIVE_CHILD_STATUSES } from "./nodes.ts";
2
3
  import { withTransaction } from "./db.ts";
3
4
  import { appendEvent } from "./events.ts";
4
5
 
@@ -64,14 +65,15 @@ export function checkLaunchBudget(
64
65
  ): BudgetDecision {
65
66
  validateRequest(request);
66
67
  const limits = normalizeLimits(options.limits);
68
+ const liveStatuses = LIVE_CHILD_STATUSES.map(() => "?").join(", ");
67
69
  const row = db
68
70
  .prepare(
69
71
  `SELECT
70
- (SELECT COUNT(*) FROM nodes WHERE parent_id IS ?) AS child_count,
72
+ (SELECT COUNT(*) FROM nodes WHERE parent_id IS ? AND status IN (${liveStatuses})) AS child_count,
71
73
  (SELECT COUNT(*) FROM nodes WHERE status IN ('running', 'starting')) AS active_count,
72
74
  CASE WHEN ? IS NULL THEN 1 ELSE EXISTS(SELECT 1 FROM nodes WHERE id = ?) END AS parent_exists`,
73
75
  )
74
- .get(request.parentId, request.parentId, request.parentId);
76
+ .get(request.parentId, ...LIVE_CHILD_STATUSES, request.parentId, request.parentId);
75
77
  const counts = {
76
78
  children: Number(row?.child_count ?? 0),
77
79
  activeNodes: Number(row?.active_count ?? 0),
@@ -86,7 +88,7 @@ export function checkLaunchBudget(
86
88
  if (counts.children >= limits.maxChildrenPerNode) {
87
89
  return denied(
88
90
  "max_children_per_node",
89
- `parent already has ${counts.children} children; maxChildrenPerNode is ${limits.maxChildrenPerNode}`,
91
+ `parent already has ${counts.children} live children; maxChildrenPerNode is ${limits.maxChildrenPerNode}`,
90
92
  base,
91
93
  );
92
94
  }
@@ -21,6 +21,13 @@ export const NON_TERMINAL_NODE_STATUSES = [
21
21
  "retired",
22
22
  ] as const satisfies readonly NodeStatus[];
23
23
 
24
+ /** Statuses that occupy a maxChildrenPerNode concurrency slot (not retired/session-only). */
25
+ export const LIVE_CHILD_STATUSES = [
26
+ "starting",
27
+ "running",
28
+ "blocked",
29
+ ] as const satisfies readonly NodeStatus[];
30
+
24
31
  export interface LegionNode {
25
32
  id: string;
26
33
  parentId: string | null;
@@ -18,7 +18,7 @@ import type { RunStore } from "./store.ts";
18
18
  import type { LegionEventBus, LegionProcessEventType } from "../extension/event-bus.ts";
19
19
  import { appendEvent } from "../legion/events.ts";
20
20
  import { withLaunchBudget, type LegionBudgetLimits } from "../legion/budget.ts";
21
- import { getChildByName, getNode, insertNode, nodeIdForChild, sanitizeNodeSlug, type LegionNode, updateNode } from "../legion/nodes.ts";
21
+ import { getChildByName, getNode, insertNode, nodeIdForChild, sanitizeNodeSlug, type LegionNode, updateNode, LIVE_CHILD_STATUSES } from "../legion/nodes.ts";
22
22
  import { upsertUsage } from "../legion/usage.ts";
23
23
  import { evaluatePhaseTransition, isLegionPhase, isTerminalPhase } from "../legion/state-machine.ts";
24
24
  import { CHILD_ACCEPTANCE_ROLE_ENV, CHILD_ROLE_ENV } from "../extension/child-guard.ts";
@@ -341,7 +341,8 @@ export class Orchestrator {
341
341
  const depthCeiling = input.utility
342
342
  ? parent.depth + 1
343
343
  : Math.min(legion.settings?.maxDepth ?? this.maxDepthValue, MAX_NESTED_PATH_ENTRIES);
344
- const childCount = Number(legion.db.prepare("SELECT COUNT(*) AS count FROM nodes WHERE parent_id = ?").get(legion.nodeId)?.count ?? 0);
344
+ const liveStatuses = LIVE_CHILD_STATUSES.map(() => "?").join(", ");
345
+ const childCount = Number(legion.db.prepare(`SELECT COUNT(*) AS count FROM nodes WHERE parent_id = ? AND status IN (${liveStatuses})`).get(legion.nodeId, ...LIVE_CHILD_STATUSES)?.count ?? 0);
345
346
  const maxChildren = previous ? Math.max(legion.settings?.maxChildrenPerNode ?? 8, childCount + 1) : legion.settings?.maxChildrenPerNode;
346
347
  const activeReservation = previous && (previous.status === "starting" || previous.status === "running") ? 1 : 0;
347
348
  const limits: LegionBudgetLimits = { maxDepth: depthCeiling, maxChildrenPerNode: maxChildren, maxActiveNodes: (legion.settings?.maxActiveNodes ?? 30) + activeReservation };