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.
@@ -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
- return `run:${path.resolve(filePath)}`;
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
- /** True when a live server accepts connections on `address` (leftover-corpse probe). */
104
- function accepting(address: string): Promise<boolean> {
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}
@@ -1,28 +1,18 @@
1
1
  import type { TeamTaskState, UsageState } from "./types.ts";
2
2
 
3
3
  /**
4
- * Lifetime usage — accumulated via message_end events, survives compaction.
5
- * cacheRead is excluded because each turn's cacheRead is the cumulative cached
6
- * prefix re-read on that one call — summing across turns would count it N times.
7
- * See: https://github.com/nichekate/pi-subagents3/issues/38
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;