pi-crew 0.11.19 → 0.11.21
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 +32 -0
- package/dist/index.mjs +479 -479
- package/package.json +1 -1
- package/skills/real-test-pi-crew/SKILL.md +25 -0
- package/src/extension/registration/subagent-manager-setup.ts +87 -17
- package/src/runtime/goal-workflow/goal-achievement.ts +47 -0
- package/src/runtime/live-session/live-session-runtime.ts +11 -48
- package/src/runtime/live-session/usage-bridge.ts +93 -0
- package/src/runtime/recovery/retry-executor.ts +53 -1
- package/src/runtime/usage-tracker.ts +41 -8
- package/src/state/coordination/locks.ts +15 -1
- package/src/state/coordination/process-lock.ts +27 -2
- package/src/state/usage.ts +15 -21
|
@@ -578,9 +578,23 @@ function runLockSocketSupplementEnabled(): boolean {
|
|
|
578
578
|
* U21: the kernel-socket lock name for a run-lock file path. Derived from the
|
|
579
579
|
* ABSOLUTE run dir path so every process agrees on the name for the same run
|
|
580
580
|
* regardless of cwd/relative-path spelling.
|
|
581
|
+
*
|
|
582
|
+
* F2 (2026-10-14): the name is prefixed with the process uid. The socket
|
|
583
|
+
* namespace is SYSTEM-WIDE (abstract sockets / named pipes are not
|
|
584
|
+
* per-user), so two different users working on the same run path would
|
|
585
|
+
* otherwise collide — user A's live holder would lock user B out of B's own
|
|
586
|
+
* run. The uid prefix removes that cross-user same-path collision. `"noid"`
|
|
587
|
+
* covers runtimes without `process.getuid` (e.g. Windows).
|
|
588
|
+
*
|
|
589
|
+
* Deploy-window: a live holder started on the OLD (uid-less) name is NOT
|
|
590
|
+
* visible to new code during the transition — SAFE, because the file lock
|
|
591
|
+
* stays the PRIMARY guard and the socket is only the U21 dual-guard
|
|
592
|
+
* supplement; once every process runs the new code, all holders use the
|
|
593
|
+
* unified uid-prefixed name.
|
|
581
594
|
*/
|
|
582
595
|
export function runLockSocketName(filePath: string): string {
|
|
583
|
-
|
|
596
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : "noid";
|
|
597
|
+
return `${uid}:run:${path.resolve(filePath)}`;
|
|
584
598
|
}
|
|
585
599
|
|
|
586
600
|
/**
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* forked children via env): pi-crew's child env allowlist strips PI_CREW_*
|
|
16
16
|
* ambient keys (§12.2.1), and nothing about the lock needs to be inherited.
|
|
17
17
|
*
|
|
18
|
+
* NAME CONVENTION: the socket/pipe namespace is system-wide, NOT per-user —
|
|
19
|
+
* callers namespace their names by uid when the protected path can be shared
|
|
20
|
+
* across users (see locks.ts `runLockSocketName`, F2 2026-10-14) so two users
|
|
21
|
+
* on the same path do not collide.
|
|
22
|
+
*
|
|
18
23
|
* VENDORED (adapted, not copied verbatim) from `@earendil-works/chord` (MIT),
|
|
19
24
|
* packages/chord/src/manager.ts:82-170 @ a4d040d30 — see NOTICE.md.
|
|
20
25
|
* Adaptations beyond renaming the namespace:
|
|
@@ -100,8 +105,20 @@ function socketAddress(name: string): SocketAddress {
|
|
|
100
105
|
return { address: fileSocketAddress(key), file: true };
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Probe whether a live server accepts connections on `address` (leftover-corpse
|
|
110
|
+
* check). A single refusal is NOT trusted on file-socket platforms: macOS can
|
|
111
|
+
* return ECONNREFUSED transiently against a LIVE listener (e.g. a momentarily
|
|
112
|
+
* full listen backlog under load — live-caught in CI 2026-10-14: a foreign
|
|
113
|
+
* child flipped a held lock to "corpse", DELETED the live socket file and
|
|
114
|
+
* rebound, stealing the lock). We require TAKEOVER_REFUSALS consecutive
|
|
115
|
+
* refusals, spaced TAKEOVER_RETRY_MS apart, before declaring a corpse.
|
|
116
|
+
*/
|
|
117
|
+
const TAKEOVER_REFUSALS = 3;
|
|
118
|
+
const TAKEOVER_RETRY_MS = 150;
|
|
119
|
+
|
|
120
|
+
/** One connect-probe: true = somebody accepts; false = refused (corpse-ish). */
|
|
121
|
+
function acceptingOnce(address: string): Promise<boolean> {
|
|
105
122
|
return new Promise((resolve) => {
|
|
106
123
|
const socket = net.connect(address);
|
|
107
124
|
socket.unref();
|
|
@@ -116,6 +133,14 @@ function accepting(address: string): Promise<boolean> {
|
|
|
116
133
|
});
|
|
117
134
|
}
|
|
118
135
|
|
|
136
|
+
async function accepting(address: string): Promise<boolean> {
|
|
137
|
+
for (let attempt = 0; attempt < TAKEOVER_REFUSALS; attempt++) {
|
|
138
|
+
if (attempt > 0) await new Promise((r) => setTimeout(r, TAKEOVER_RETRY_MS));
|
|
139
|
+
if (await acceptingOnce(address)) return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
119
144
|
/**
|
|
120
145
|
* Acquire the process lock for `name`. Resolves with a handle once THIS process
|
|
121
146
|
* owns the kernel socket; rejects with {@link ProcessLockHeldError}
|
package/src/state/usage.ts
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
import type { TeamTaskState, UsageState } from "./types.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Usage aggregation helpers for the run ledger and cost reporting.
|
|
5
|
+
*
|
|
6
|
+
* The ledger (aggregateUsage / RoleUsage) carries the FULL 5-field usage set
|
|
7
|
+
* (input, output, cacheRead, cacheWrite, cost) for accurate cost attribution.
|
|
8
|
+
* The canonical 5-field `LifetimeUsage` type lives in `src/runtime/usage-tracker.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Token-sum displays and the budget proxy deliberately EXCLUDE cacheRead:
|
|
11
|
+
* each turn's cacheRead is the cumulative cached prefix re-read on that one
|
|
12
|
+
* call — summing across turns would count it N times.
|
|
13
|
+
* See: https://github.com/nichekate/pi-subagents3/issues/38 and
|
|
14
|
+
* docs/archive/pi-subagents3-deep-analysis.md
|
|
8
15
|
*/
|
|
9
|
-
export type LifetimeUsage = {
|
|
10
|
-
input: number;
|
|
11
|
-
output: number;
|
|
12
|
-
cacheWrite: number;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/** Sum of lifetime usage components, or 0 if undefined. */
|
|
16
|
-
export function getLifetimeTotal(u?: LifetimeUsage): number {
|
|
17
|
-
return u ? u.input + u.output + u.cacheWrite : 0;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Add a usage delta into a target accumulator (mutates target). */
|
|
21
|
-
export function addUsage(into: LifetimeUsage, delta: LifetimeUsage): void {
|
|
22
|
-
into.input += delta.input;
|
|
23
|
-
into.output += delta.output;
|
|
24
|
-
into.cacheWrite += delta.cacheWrite;
|
|
25
|
-
}
|
|
26
16
|
|
|
27
17
|
export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
|
|
28
18
|
const total: UsageState = {};
|
|
@@ -71,6 +61,8 @@ export interface RoleUsage {
|
|
|
71
61
|
role: string;
|
|
72
62
|
input: number;
|
|
73
63
|
output: number;
|
|
64
|
+
/** Ledger field — full cacheRead sum for cost attribution (not shown in token-sum displays). */
|
|
65
|
+
cacheRead: number;
|
|
74
66
|
cacheWrite: number;
|
|
75
67
|
cost: number;
|
|
76
68
|
turns: number;
|
|
@@ -88,6 +80,7 @@ export function aggregateUsageByRole(tasks: TeamTaskState[]): RoleUsage[] {
|
|
|
88
80
|
role,
|
|
89
81
|
input: 0,
|
|
90
82
|
output: 0,
|
|
83
|
+
cacheRead: 0,
|
|
91
84
|
cacheWrite: 0,
|
|
92
85
|
cost: 0,
|
|
93
86
|
turns: 0,
|
|
@@ -99,6 +92,7 @@ export function aggregateUsageByRole(tasks: TeamTaskState[]): RoleUsage[] {
|
|
|
99
92
|
if (!task.usage) continue;
|
|
100
93
|
bucket.input += task.usage.input ?? 0;
|
|
101
94
|
bucket.output += task.usage.output ?? 0;
|
|
95
|
+
bucket.cacheRead += task.usage.cacheRead ?? 0;
|
|
102
96
|
bucket.cacheWrite += task.usage.cacheWrite ?? 0;
|
|
103
97
|
bucket.cost += task.usage.cost ?? 0;
|
|
104
98
|
bucket.turns += task.usage.turns ?? 0;
|