pi-crew 0.9.58 → 0.9.59

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
@@ -3,6 +3,23 @@
3
3
  > **Note:** `atomic-write-v2.ts` / `AtomicWriter` mentioned in historical entries below was consolidated into `atomic-write.ts` as of v0.9.42. This changelog is preserved as historical record — the migration was completed (the v2 class was never adopted; v1 won on simplicity + symlink-safety + link+unlink atomicity). See `docs/migration/atomic-write-v2-migration.md` for the decision rationale.
4
4
 
5
5
 
6
+ ## [0.9.59] — cross-session isolation: stop leak of runs/subagents between concurrent pi sessions + stop false-reap of live sessions (2026-08-05)
7
+
8
+ ### Bug fixes
9
+
10
+ - **Chạy 2 pi session trên cùng repo → thông tin run/subagent của session A rò rỉ sang B, và crash-recovery của B giết foreground work đang chạy của A.** Hai lỗi gốc: (1) `extractSessionId()` đọc `ctx.sessionId` (own property) — không tồn tại trên pi 0.83.0 `ExtensionContext` → trả `undefined` → mọi session filter vô hiệu thầm lặng; (2) crash-recovery + shared-state listings không có session component, nên session đang sống-but-bận không phân biệt được với session đã crash.
11
+ - **`extractSessionId` (`src/utils/session-utils.ts`)**: giờ dùng `ctx.sessionManager.getSessionId()` (WeakMap cache keyed bởi `sessionManager` ref ổn định — `ctx` được tạo mới mỗi event nên không thể làm cache key), giữ descriptor lookup làm fallback cho test mock/pi cũ.
12
+ - **Crash-recovery (`src/runtime/recovery/crash-recovery.ts`)**: `reconcileAllStaleRuns` / `purgeStaleActiveRunIndex` / `detectInterruptedRuns` nhận `currentSessionId?` và **skip run của chính session đang sống** (`===`, theo pattern `cancelOrphanedRuns`). Session chết vẫn được dọn; back-compat giữ nguyên khi `currentSessionId` undefined. Thread qua tất cả caller incl. path tần suất cao `observability` (`before_agent_start` + interval 5-min) và `lazy-configurers`.
13
+ - **Subagents (`src/runtime/subagent-manager.ts`, `subagent-tools.ts`, `subagent-manager-setup.ts`)**: `SubagentRecord` thêm `ownerSessionId`; `get_subagent_result` từ chối record của session khác (record cũ vẫn serve); `resultConsumed` không còn bị clobber chéo session; agent id thêm entropy tránh collision cross-process; `isOwnerSessionCurrent` check `ownerSessionId` cross-process (giữ generation cho in-process switch).
14
+ - **UI (`src/ui/run-dashboard.ts`, `widget/*`, `powerbar-publisher.ts`)**: dashboard `refreshRuns`, widget render, powerbar re-apply filter `workspaceId` mỗi frame. Powerbar self-derive `workspaceId` qua `extractSessionId(ctx)` nên mọi caller hiện tại đều được.
15
+ - **Notifier/session-summary (`src/extension/async-notifier.ts`, `session-summary.ts`)**: filter `listRuns` theo `ownerSessionId` trước notify — session B không còn toast về run của A.
16
+ - **Health filter (#3)**: `ctx.currentCtx?.sessionManager?.getSessionId()` + bỏ dead clause `ownerSessionGeneration` (field không tồn tại trên `TeamRunManifest`) — trước đó silently drop tất cả owned runs.
17
+ - Verified end-to-end theo skill `real-test-pi-crew`: `test:critical` 101/101, 3-path kill-switch green, typecheck + bundle + md5 sync OK, live TUI (tmux + pty) render không crash, smoke verifier 52s (<300s, no hang), full feature battery (team tool 9a–9f + subagent tools) clean — zero `Unknown type`/`Validation failed`. 6837 unit + 214 integration tests pass.
18
+ - Lưu ý: `#12` (DeliveryCoordinator) infrastructure staged nhưng inert — `deliver*` không có production caller, nên queue không bao giờ được feed. Documented trong `docs/cross-session-leak-fix-plan.md`.
19
+
20
+ ### Docs
21
+ - `docs/cross-session-leak-audit.md` (audit re-verify: 2/2 root cause CONFIRMED, 12/13 vector CONFIRMED, #3 REFUTED) + `docs/cross-session-leak-fix-plan.md` (phased plan, reviewed).
22
+
6
23
  ## [0.9.58] — fix load crash on stale hoisted typebox: defensive guard + bundle vendoring (survives `pi update`) + round-1/round-2 fixes (2026-08-04)
7
24
 
8
25
  ### Bug fixes
package/dist/index.mjs CHANGED
@@ -12871,14 +12871,23 @@ function toPiSessionId(runId) {
12871
12871
  }
12872
12872
  function extractSessionId(ctx) {
12873
12873
  if (typeof ctx !== "object" || ctx === null) return void 0;
12874
- let raw;
12875
12874
  try {
12876
- raw = Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value;
12875
+ const sm = ctx.sessionManager;
12876
+ if (sm && typeof sm === "object") {
12877
+ const cached2 = sessionIdCache.get(sm);
12878
+ if (cached2) return cached2;
12879
+ const id = sm.getSessionId?.();
12880
+ if (typeof id === "string" && id.length > 0) {
12881
+ sessionIdCache.set(sm, id);
12882
+ return id;
12883
+ }
12884
+ }
12885
+ const direct = Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value;
12886
+ if (typeof direct === "string" && direct.length > 0) return direct;
12877
12887
  } catch {
12878
12888
  return void 0;
12879
12889
  }
12880
- if (typeof raw !== "string" || raw.length === 0) return void 0;
12881
- return raw;
12890
+ return void 0;
12882
12891
  }
12883
12892
  function extractBrokerSessionId(ctx) {
12884
12893
  if (typeof ctx !== "object" || ctx === null) return void 0;
@@ -12893,9 +12902,11 @@ function extractBrokerSessionId(ctx) {
12893
12902
  return void 0;
12894
12903
  }
12895
12904
  }
12905
+ var sessionIdCache;
12896
12906
  var init_session_utils = __esm({
12897
12907
  "src/utils/session-utils.ts"() {
12898
12908
  "use strict";
12909
+ sessionIdCache = /* @__PURE__ */ new WeakMap();
12899
12910
  }
12900
12911
  });
12901
12912
 
@@ -42694,8 +42705,8 @@ function registerPiCrewPowerbarSegments(events, config) {
42694
42705
  label: "pi-crew workflow steps"
42695
42706
  });
42696
42707
  }
42697
- function updatePiCrewPowerbar(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount = 0, preloadedManifests) {
42698
- powerbarPublisher.update(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount, preloadedManifests);
42708
+ function updatePiCrewPowerbar(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount = 0, preloadedManifests, workspaceId) {
42709
+ powerbarPublisher.update(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount, preloadedManifests, workspaceId);
42699
42710
  }
42700
42711
  function powerbarKey(payload) {
42701
42712
  return `${payload.text ?? ""}|${payload.suffix ?? ""}|${payload.bar ?? ""}|${payload.color ?? ""}|${payload.icon ?? ""}|${payload.barSegments ?? ""}`;
@@ -42739,8 +42750,8 @@ function buildStepsPayload(active, allTasks) {
42739
42750
  color
42740
42751
  };
42741
42752
  }
42742
- function requestPowerbarUpdate(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount = 0, preloadedManifests) {
42743
- powerbarPublisher.request(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount, preloadedManifests);
42753
+ function requestPowerbarUpdate(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount = 0, preloadedManifests, workspaceId) {
42754
+ powerbarPublisher.request(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount, preloadedManifests, workspaceId);
42744
42755
  }
42745
42756
  function disposePowerbarCoalescer() {
42746
42757
  powerbarPublisher.dispose();
@@ -42762,6 +42773,7 @@ var init_powerbar_publisher = __esm({
42762
42773
  init_usage();
42763
42774
  init_file_coalescer();
42764
42775
  init_internal_error();
42776
+ init_session_utils();
42765
42777
  init_discover_workflows();
42766
42778
  init_render_coalescer();
42767
42779
  init_widget_formatters();
@@ -42777,13 +42789,25 @@ var init_powerbar_publisher = __esm({
42777
42789
  const a = this.#latestArgs;
42778
42790
  this.#latestArgs = null;
42779
42791
  if (!a) return;
42780
- this.update(a.events, a.cwd, a.config, a.manifestCache, a.snapshotCache, a.ctx, a.notificationCount, a.preloadedManifests);
42792
+ this.update(
42793
+ a.events,
42794
+ a.cwd,
42795
+ a.config,
42796
+ a.manifestCache,
42797
+ a.snapshotCache,
42798
+ a.ctx,
42799
+ a.notificationCount,
42800
+ a.preloadedManifests,
42801
+ a.workspaceId
42802
+ );
42781
42803
  }, 200);
42782
42804
  }
42783
- update(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount = 0, preloadedManifests) {
42805
+ update(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount = 0, preloadedManifests, workspaceId) {
42784
42806
  if (config?.powerbar === false) return;
42785
42807
  const useStatusFallback = !hasPowerbarConsumer(events);
42786
- const runs = preloadedManifests ?? (manifestCache2 ? manifestCache2.list(20) : listRecentRuns(cwd, 20));
42808
+ const effectiveWorkspaceId = workspaceId ?? extractSessionId(ctx);
42809
+ const allRuns = preloadedManifests ?? (manifestCache2 ? manifestCache2.list(20) : listRecentRuns(cwd, 20));
42810
+ const runs = effectiveWorkspaceId ? allRuns.filter((run) => !run.ownerSessionId || run.ownerSessionId === effectiveWorkspaceId) : allRuns;
42787
42811
  const active = runs.map((run) => {
42788
42812
  let snapshot;
42789
42813
  try {
@@ -42839,7 +42863,8 @@ var init_powerbar_publisher = __esm({
42839
42863
  const tokenTotal = hasUsage ? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0) : snapshotTokens;
42840
42864
  const model = config?.showModel === false ? void 0 : agents.find((agent) => agent.model)?.model?.split("/").at(-1);
42841
42865
  const tokenText = config?.showTokens === false || !tokenTotal ? void 0 : compactTokens(tokenTotal);
42842
- const liveRunning = listLiveAgents().filter((a) => a.status === "running").length;
42866
+ const liveAgents2 = effectiveWorkspaceId ? listLiveAgentsByWorkspace(effectiveWorkspaceId) : listLiveAgents();
42867
+ const liveRunning = liveAgents2.filter((a) => a.status === "running").length;
42843
42868
  const runningCount = agents.filter((a) => a.status === "running").length;
42844
42869
  const queuedCount = active.reduce(
42845
42870
  (sum, item) => sum + item.tasks.reduce((s, t2) => s + (t2.status === "queued" || t2.status === "waiting" ? 1 : 0), 0),
@@ -42893,7 +42918,7 @@ var init_powerbar_publisher = __esm({
42893
42918
  * into a single render pass within 200ms, preventing UI flicker from
42894
42919
  * event bursts.
42895
42920
  */
42896
- request(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount = 0, preloadedManifests) {
42921
+ request(events, cwd, config, manifestCache2, snapshotCache, ctx, notificationCount = 0, preloadedManifests, workspaceId) {
42897
42922
  if (config?.powerbar === false) return;
42898
42923
  this.#latestArgs = {
42899
42924
  events,
@@ -42903,7 +42928,8 @@ var init_powerbar_publisher = __esm({
42903
42928
  snapshotCache,
42904
42929
  ctx,
42905
42930
  notificationCount,
42906
- preloadedManifests
42931
+ preloadedManifests,
42932
+ workspaceId
42907
42933
  };
42908
42934
  this.#coalescer.request();
42909
42935
  }
@@ -44611,12 +44637,13 @@ function shouldRecoverTask(task, deadMs) {
44611
44637
  if (!task.heartbeat) return true;
44612
44638
  return task.heartbeat.alive === false || isWorkerHeartbeatStale(task.heartbeat, deadMs);
44613
44639
  }
44614
- function detectInterruptedRuns(cwd, manifestCache2, deadMs = 3e5) {
44640
+ function detectInterruptedRuns(cwd, manifestCache2, deadMs = 3e5, currentSessionId) {
44615
44641
  const plans = [];
44616
44642
  for (const manifest of manifestCache2.list(50)) {
44617
44643
  if (manifest.status !== "running" && manifest.status !== "blocked") continue;
44618
44644
  if (isPlanApprovalPending(manifest)) continue;
44619
44645
  if (manifest.async?.pid !== void 0 && checkProcessLiveness(manifest.async.pid).alive) continue;
44646
+ if (currentSessionId && manifest.ownerSessionId && manifest.ownerSessionId === currentSessionId) continue;
44620
44647
  const loaded = loadRunManifestById(cwd, manifest.runId);
44621
44648
  if (!loaded) continue;
44622
44649
  const resumableTasks = loaded.tasks.filter((task) => shouldRecoverTask(task, deadMs)).map((task) => task.id);
@@ -44788,7 +44815,7 @@ function hasRecentLifeEvidence(entry, manifestUpdatedAt, now, staleThresholdMs)
44788
44815
  if (Number.isFinite(hbAge) && hbAge <= staleThresholdMs) return true;
44789
44816
  return false;
44790
44817
  }
44791
- function purgeStaleActiveRunIndex(staleThresholdMs = 3e5, now = Date.now()) {
44818
+ function purgeStaleActiveRunIndex(staleThresholdMs = 3e5, now = Date.now(), currentSessionId) {
44792
44819
  const purged = [];
44793
44820
  const kept = [];
44794
44821
  const entries = readActiveRunRegistry();
@@ -44830,6 +44857,10 @@ function purgeStaleActiveRunIndex(staleThresholdMs = 3e5, now = Date.now()) {
44830
44857
  purged.push(entry.runId);
44831
44858
  continue;
44832
44859
  }
44860
+ if (currentSessionId && manifest?.ownerSessionId && manifest.ownerSessionId === currentSessionId) {
44861
+ kept.push(entry.runId);
44862
+ continue;
44863
+ }
44833
44864
  if (manifest?.status === "running" && manifest.async?.pid !== void 0) {
44834
44865
  const pidAlive = checkProcessLiveness(manifest.async.pid).alive;
44835
44866
  if (!pidAlive && !hasRecentLifeEvidence(entry, manifest.updatedAt, now, staleThresholdMs)) {
@@ -44930,9 +44961,13 @@ function purgeStaleActiveRunIndex(staleThresholdMs = 3e5, now = Date.now()) {
44930
44961
  }
44931
44962
  return { purged, kept };
44932
44963
  }
44933
- function reconcileAllStaleRuns(cwd, manifestCache2, now = Date.now()) {
44964
+ function reconcileAllStaleRuns(cwd, manifestCache2, now = Date.now(), currentSessionId) {
44934
44965
  const results = [];
44935
- const runIds = manifestCache2.list(50).filter((m) => m.status === "running" || m.status === "blocked").map((m) => m.runId);
44966
+ const runIds = manifestCache2.list(50).filter((m) => {
44967
+ if (m.status !== "running" && m.status !== "blocked") return false;
44968
+ if (currentSessionId && m.ownerSessionId && m.ownerSessionId === currentSessionId) return false;
44969
+ return true;
44970
+ }).map((m) => m.runId);
44936
44971
  for (const runId of runIds) {
44937
44972
  const cached2 = manifestCache2.get(runId);
44938
44973
  if (!cached2) continue;
@@ -45016,7 +45051,7 @@ function activeWidgetRuns(cwd, manifestCache2, snapshotCache, preloadedManifests
45016
45051
  if (now - lastStaleReconcileAt > STALE_RECONCILE_INTERVAL_MS && manifestCache2) {
45017
45052
  lastStaleReconcileAt = now;
45018
45053
  try {
45019
- reconcileAllStaleRuns(cwd, manifestCache2);
45054
+ reconcileAllStaleRuns(cwd, manifestCache2, Date.now(), workspaceId);
45020
45055
  } catch {
45021
45056
  }
45022
45057
  }
@@ -45458,7 +45493,8 @@ function updateCrewWidget(ctx, state, config, manifestCache2, snapshotCache, pre
45458
45493
  notificationCount: state.notificationCount ?? 0,
45459
45494
  manifestCache: manifestCache2,
45460
45495
  snapshotCache,
45461
- preloadManifests: preloadedManifests
45496
+ preloadManifests: preloadedManifests,
45497
+ workspaceId
45462
45498
  };
45463
45499
  else {
45464
45500
  state.model.cwd = ctx.cwd;
@@ -45468,6 +45504,7 @@ function updateCrewWidget(ctx, state, config, manifestCache2, snapshotCache, pre
45468
45504
  state.model.manifestCache = manifestCache2;
45469
45505
  state.model.snapshotCache = snapshotCache;
45470
45506
  state.model.preloadManifests = preloadedManifests;
45507
+ state.model.workspaceId = workspaceId;
45471
45508
  }
45472
45509
  if (needsWidgetInstall) {
45473
45510
  const model = state.model;
@@ -45603,7 +45640,13 @@ var init_widget = __esm({
45603
45640
  if (activeResizeTarget === this) activeResizeTarget = void 0;
45604
45641
  }
45605
45642
  render(width) {
45606
- const runs = activeWidgetRuns(this.model.cwd, this.model.manifestCache, this.model.snapshotCache, this.model.preloadManifests);
45643
+ const runs = activeWidgetRuns(
45644
+ this.model.cwd,
45645
+ this.model.manifestCache,
45646
+ this.model.snapshotCache,
45647
+ this.model.preloadManifests,
45648
+ this.model.workspaceId
45649
+ );
45607
45650
  const now = Date.now();
45608
45651
  let sigBase;
45609
45652
  if (this.cachedBuildSignature !== "" && now - this.cachedBuildSignatureAt < SIGNATURE_CACHE_TTL_MS) {
@@ -64582,7 +64625,8 @@ var init_run_dashboard = __esm({
64582
64625
  if (!this.options.runProvider) return;
64583
64626
  const selectedRunId = this.selectedRunId();
64584
64627
  const next = this.options.runProvider();
64585
- this.runs = Array.isArray(next) ? next : this.runs;
64628
+ const unfiltered = Array.isArray(next) ? next : this.runs;
64629
+ this.runs = this.options.workspaceId ? unfiltered.filter((run) => !run.ownerSessionId || run.ownerSessionId === this.options.workspaceId) : unfiltered;
64586
64630
  if (selectedRunId) {
64587
64631
  const nextIndex = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).findIndex((row) => row.run?.runId === selectedRunId);
64588
64632
  if (nextIndex >= 0) this.selected = nextIndex;
@@ -67808,6 +67852,7 @@ var init_commands = __esm({
67808
67852
  });
67809
67853
 
67810
67854
  // src/runtime/subagent-manager.ts
67855
+ import { randomUUID as randomUUID8 } from "node:crypto";
67811
67856
  import * as fs100 from "node:fs";
67812
67857
  import * as path81 from "node:path";
67813
67858
  function isValidSubagentId(id) {
@@ -67926,6 +67971,7 @@ var init_subagent_manager = __esm({
67926
67971
  "resultConsumed",
67927
67972
  "background",
67928
67973
  "ownerSessionGeneration",
67974
+ "ownerSessionId",
67929
67975
  "stuckNotified",
67930
67976
  "blockedAt",
67931
67977
  "turnCount",
@@ -67952,7 +67998,7 @@ var init_subagent_manager = __esm({
67952
67998
  }
67953
67999
  spawn(options, runner, signal) {
67954
68000
  const record = {
67955
- id: `agent_${Date.now().toString(36)}_${(++this.counter).toString(36)}`,
68001
+ id: `agent_${Date.now().toString(36)}_${randomUUID8().slice(0, 8)}_${(++this.counter).toString(36)}`,
67956
68002
  type: options.type,
67957
68003
  description: options.description,
67958
68004
  prompt: options.prompt,
@@ -67962,6 +68008,7 @@ var init_subagent_manager = __esm({
67962
68008
  skill: options.skill,
67963
68009
  background: options.background,
67964
68010
  ownerSessionGeneration: options.ownerSessionGeneration,
68011
+ ownerSessionId: options.ownerSessionId,
67965
68012
  batchId: options.batchId
67966
68013
  };
67967
68014
  this.records.set(record.id, record);
@@ -68214,7 +68261,8 @@ var init_subagent_manager = __esm({
68214
68261
  id: current.id,
68215
68262
  runId: current.runId,
68216
68263
  durationMs: Math.max(0, Date.now() - current.blockedAt),
68217
- ownerSessionGeneration: current.ownerSessionGeneration
68264
+ ownerSessionGeneration: current.ownerSessionGeneration,
68265
+ ownerSessionId: current.ownerSessionId
68218
68266
  });
68219
68267
  savePersistedSubagentRecord(cwd, current);
68220
68268
  };
@@ -68878,7 +68926,9 @@ function startAsyncRunNotifier(ctx, state, intervalMs = 5e3, options = {}) {
68878
68926
  state.generation = generation;
68879
68927
  const startedAtMs = Date.now();
68880
68928
  const staleBeforeMs = state.lastStoppedAtMs ?? startedAtMs;
68881
- for (const run of listRuns(ctx.cwd)) {
68929
+ const sid = extractSessionId(ctx);
68930
+ const ownsRun = (run) => !sid || !run.ownerSessionId || run.ownerSessionId === sid;
68931
+ for (const run of listRuns(ctx.cwd).filter(ownsRun)) {
68882
68932
  const updatedAtMs = timeMs(run.updatedAt) ?? 0;
68883
68933
  if (isFinished2(run.status) && updatedAtMs < staleBeforeMs) state.seenFinishedRunIds.add(run.runId);
68884
68934
  }
@@ -68888,7 +68938,7 @@ function startAsyncRunNotifier(ctx, state, intervalMs = 5e3, options = {}) {
68888
68938
  if (options.isCurrent && !options.isCurrent(generation)) return;
68889
68939
  const nowMs3 = Date.now();
68890
68940
  if (cachedRuns === void 0 || nowMs3 - (state.lastListRunsMs ?? 0) > LIST_RUNS_DEBOUNCE_MS) {
68891
- cachedRuns = listRuns(ctx.cwd).slice(0, 20);
68941
+ cachedRuns = listRuns(ctx.cwd).filter(ownsRun).slice(0, 20);
68892
68942
  state.lastListRunsMs = nowMs3;
68893
68943
  }
68894
68944
  for (const run of cachedRuns) {
@@ -68925,6 +68975,7 @@ var init_async_notifier = __esm({
68925
68975
  init_event_log();
68926
68976
  init_state_store();
68927
68977
  init_internal_error();
68978
+ init_session_utils();
68928
68979
  init_run_index();
68929
68980
  LIST_RUNS_DEBOUNCE_MS = 3e4;
68930
68981
  }
@@ -69137,11 +69188,15 @@ var init_delivery_coordinator = __esm({
69137
69188
  deps;
69138
69189
  ttlTimer;
69139
69190
  timerStarted = false;
69191
+ /** The session id passed to the most recent activate(); used by flushQueuedResults
69192
+ * to park deliveries owned by a different session (vector #12). */
69193
+ activeSessionId;
69140
69194
  constructor(deps) {
69141
69195
  this.deps = deps;
69142
69196
  }
69143
69197
  activate(sessionId) {
69144
69198
  this.active = true;
69199
+ this.activeSessionId = sessionId;
69145
69200
  this.flushQueuedResults();
69146
69201
  }
69147
69202
  deactivate() {
@@ -69154,7 +69209,7 @@ var init_delivery_coordinator = __esm({
69154
69209
  getPendingCount() {
69155
69210
  return this.pending.length;
69156
69211
  }
69157
- deliverResult(runId, result4) {
69212
+ deliverResult(runId, result4, ownerSessionId) {
69158
69213
  if (this.active && this.deps.emit) {
69159
69214
  try {
69160
69215
  this.deps.emit("pi-crew:run-result", result4);
@@ -69168,10 +69223,11 @@ var init_delivery_coordinator = __esm({
69168
69223
  runId,
69169
69224
  payload: result4,
69170
69225
  timestamp: Date.now(),
69171
- type: "result"
69226
+ type: "result",
69227
+ ownerSessionId
69172
69228
  });
69173
69229
  }
69174
- deliverNotification(notification) {
69230
+ deliverNotification(notification, ownerSessionId) {
69175
69231
  let delivered = false;
69176
69232
  if (this.active && this.deps.sendFollowUp) {
69177
69233
  try {
@@ -69195,10 +69251,11 @@ var init_delivery_coordinator = __esm({
69195
69251
  runId: notification.runId ?? "",
69196
69252
  payload: notification,
69197
69253
  timestamp: Date.now(),
69198
- type: "notification"
69254
+ type: "notification",
69255
+ ownerSessionId
69199
69256
  });
69200
69257
  }
69201
- deliverSteer(runId, message) {
69258
+ deliverSteer(runId, message, ownerSessionId) {
69202
69259
  if (this.active && this.deps.sendWakeUp) {
69203
69260
  try {
69204
69261
  this.deps.sendWakeUp(message);
@@ -69212,7 +69269,8 @@ var init_delivery_coordinator = __esm({
69212
69269
  runId,
69213
69270
  payload: message,
69214
69271
  timestamp: Date.now(),
69215
- type: "steer"
69272
+ type: "steer",
69273
+ ownerSessionId
69216
69274
  });
69217
69275
  }
69218
69276
  flushQueuedResults() {
@@ -69223,6 +69281,10 @@ var init_delivery_coordinator = __esm({
69223
69281
  try {
69224
69282
  const retryLater = [];
69225
69283
  for (const delivery of batch) {
69284
+ if (delivery.ownerSessionId && this.activeSessionId && delivery.ownerSessionId !== this.activeSessionId) {
69285
+ retryLater.push(delivery);
69286
+ continue;
69287
+ }
69226
69288
  if (delivery.type === "steer" && delivery.generation !== void 0 && delivery.generation !== this.generation) {
69227
69289
  logInternalError("delivery-coordinator.flush.stale", void 0, `runId=${delivery.runId} type=${delivery.type}`);
69228
69290
  continue;
@@ -70659,7 +70721,7 @@ async function configureObservability(ctx, state, deps) {
70659
70721
  deps.pi.on?.("before_agent_start", () => {
70660
70722
  if (deps.isCleanedUp()) return;
70661
70723
  try {
70662
- deps.reconcileStaleRuns(ctx.cwd, deps.getManifestCache(ctx.cwd));
70724
+ deps.reconcileStaleRuns(ctx.cwd, deps.getManifestCache(ctx.cwd), extractSessionId(ctx));
70663
70725
  } catch (error) {
70664
70726
  logInternalError("register.autoRepair.turnHook", error);
70665
70727
  }
@@ -70671,7 +70733,7 @@ async function configureObservability(ctx, state, deps) {
70671
70733
  state.autoRepairTimer = setInterval(() => {
70672
70734
  if (deps.isCleanedUp()) return;
70673
70735
  try {
70674
- const staleResults = deps.reconcileStaleRuns(ctx.cwd, deps.getManifestCache(ctx.cwd));
70736
+ const staleResults = deps.reconcileStaleRuns(ctx.cwd, deps.getManifestCache(ctx.cwd), extractSessionId(ctx));
70675
70737
  if (Array.isArray(staleResults) && staleResults.length > 0) {
70676
70738
  for (const result4 of staleResults) {
70677
70739
  const repaired = result4.repaired;
@@ -70729,7 +70791,8 @@ async function configureObservability(ctx, state, deps) {
70729
70791
  const cacheSnapshot = deps.getManifestCache(cwdSnapshot);
70730
70792
  void deps.importCrashRecovery().then(({ detectInterruptedRuns: detectInterruptedRuns2 }) => {
70731
70793
  if (deps.isCleanedUp()) return;
70732
- for (const plan of detectInterruptedRuns2(cwdSnapshot, cacheSnapshot)) {
70794
+ const sid = extractSessionId(ctx);
70795
+ for (const plan of detectInterruptedRuns2(cwdSnapshot, cacheSnapshot, 3e5, sid)) {
70733
70796
  deps.notifyOperator({
70734
70797
  id: `recovery_prompt_${plan.runId}`,
70735
70798
  severity: "warning",
@@ -70769,6 +70832,7 @@ var init_observability = __esm({
70769
70832
  init_config();
70770
70833
  init_internal_error();
70771
70834
  init_paths();
70835
+ init_session_utils();
70772
70836
  }
70773
70837
  });
70774
70838
 
@@ -74346,7 +74410,10 @@ function buildRegistrationContext(pi) {
74346
74410
  globalStore: globalThis,
74347
74411
  runtimeCleanupStoreKey: RUNTIME_CLEANUP_STORE_KEY,
74348
74412
  captureSessionGeneration: () => ctx.sessionGeneration,
74349
- isOwnerSessionCurrent: (gen) => !ctx.cleanedUp && (gen === void 0 || gen === ctx.sessionGeneration),
74413
+ isOwnerSessionCurrent: (gen, oid) => {
74414
+ const currentSid = ctx.currentCtx?.sessionManager?.getSessionId?.();
74415
+ return !ctx.cleanedUp && (oid === void 0 || oid === currentSid) && (gen === void 0 || gen === ctx.sessionGeneration);
74416
+ },
74350
74417
  isContextCurrent: (c, gen) => !ctx.cleanedUp && ctx.currentCtx === c && ctx.sessionGeneration === gen,
74351
74418
  telemetryEnabled: () => loadConfig(ctx.currentCtx?.cwd ?? process.cwd()).config.telemetry?.enabled !== false,
74352
74419
  notifyOperator: void 0,
@@ -74358,7 +74425,7 @@ function buildRegistrationContext(pi) {
74358
74425
  configureObservability: () => void 0,
74359
74426
  configureDeliveryCoordinator: () => void 0,
74360
74427
  importCrashRecovery: void 0,
74361
- purgeStaleActiveRunIndexSyncIfLoaded: () => void 0,
74428
+ purgeStaleActiveRunIndexSyncIfLoaded: (_currentSessionId) => void 0,
74362
74429
  startForegroundRun: void 0,
74363
74430
  abortForegroundRun: () => false,
74364
74431
  openLiveSidebar: () => void 0,
@@ -74405,10 +74472,10 @@ async function importCrashRecovery() {
74405
74472
  }
74406
74473
  return _cachedCrashRecovery;
74407
74474
  }
74408
- function purgeStaleActiveRunIndexSyncIfLoaded() {
74475
+ function purgeStaleActiveRunIndexSyncIfLoaded(currentSessionId) {
74409
74476
  if (!_cachedCrashRecovery) return;
74410
74477
  try {
74411
- _cachedCrashRecovery.purgeStaleActiveRunIndex();
74478
+ _cachedCrashRecovery.purgeStaleActiveRunIndex(3e5, Date.now(), currentSessionId);
74412
74479
  } catch (error) {
74413
74480
  logInternalError("register.cleanupRuntime.purgeStale", error);
74414
74481
  }
@@ -74761,7 +74828,7 @@ async function configureObservabilityImpl(pi, ctx, extCtx) {
74761
74828
  getManifestCache: ctx.getManifestCache,
74762
74829
  notifyOperator: ctx.notifyOperator,
74763
74830
  isCleanedUp: () => ctx.cleanedUp,
74764
- reconcileStaleRuns: (cwd, cache3) => reconcileAllStaleRuns(cwd, cache3),
74831
+ reconcileStaleRuns: (cwd, cache3, currentSessionId) => reconcileAllStaleRuns(cwd, cache3, void 0, currentSessionId),
74765
74832
  reconcileOrphanedTempWorkspaces: (now, opts) => reconcileOrphanedTempWorkspaces(now, opts),
74766
74833
  cleanupOrphanTempDirs,
74767
74834
  cleanupLegacyOrphanTempDirs,
@@ -74805,9 +74872,9 @@ import * as fsp3 from "node:fs/promises";
74805
74872
  import * as net2 from "node:net";
74806
74873
 
74807
74874
  // src/runtime/broker/crew-broker-tokens.ts
74808
- import { randomUUID as randomUUID8, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
74875
+ import { randomUUID as randomUUID9, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
74809
74876
  function newBrokerToken() {
74810
- return randomUUID8();
74877
+ return randomUUID9();
74811
74878
  }
74812
74879
  var BrokerTokenRegistry = class _BrokerTokenRegistry {
74813
74880
  map = /* @__PURE__ */ new Map();
@@ -76312,9 +76379,12 @@ function registerCrewAutocomplete(ctx) {
76312
76379
  // src/extension/session-summary.ts
76313
76380
  init_crew_agent_records();
76314
76381
  init_process_status();
76382
+ init_session_utils();
76315
76383
  init_run_index();
76316
76384
  function notifyActiveRuns(ctx) {
76385
+ const sid = extractSessionId(ctx);
76317
76386
  const active = listRuns(ctx.cwd).filter((run) => {
76387
+ if (sid && run.ownerSessionId && run.ownerSessionId !== sid) return false;
76318
76388
  if (run.status !== "queued" && run.status !== "planning" && run.status !== "running") return false;
76319
76389
  const agents = readCrewAgents(run);
76320
76390
  return isDisplayActiveRun(run, agents);
@@ -76537,7 +76607,7 @@ async function runDeferredSessionCleanup(pi, ctx, ownerGeneration, currentSessio
76537
76607
  logInternalError("register.sessionStart.orphanWorkers", error);
76538
76608
  }
76539
76609
  try {
76540
- const { purged } = purgeStaleActiveRunIndexFn();
76610
+ const { purged } = purgeStaleActiveRunIndexFn(3e5, Date.now(), currentSessionId);
76541
76611
  if (purged.length > 0) {
76542
76612
  ctx.notifyOperator({
76543
76613
  id: `active_index_purge`,
@@ -76551,7 +76621,7 @@ async function runDeferredSessionCleanup(pi, ctx, ownerGeneration, currentSessio
76551
76621
  logInternalError("register.sessionStart.globalIndexPurge", error);
76552
76622
  }
76553
76623
  try {
76554
- const staleResults = reconcileAllStaleRuns(extensionCtx.cwd, ctx.getManifestCache(extensionCtx.cwd)) ?? [];
76624
+ const staleResults = reconcileAllStaleRuns(extensionCtx.cwd, ctx.getManifestCache(extensionCtx.cwd), Date.now(), currentSessionId) ?? [];
76555
76625
  if (staleResults.length > 0) {
76556
76626
  ctx.notifyOperator({
76557
76627
  id: "stale_reconcile",
@@ -76671,6 +76741,9 @@ function setupCrewScheduler(pi, ctx, extensionCtx, sessionId) {
76671
76741
  });
76672
76742
  return crewScheduler;
76673
76743
  }
76744
+ function filterManifestsForHealthNotifications(manifests, currentSessionId) {
76745
+ return manifests.filter((run) => !run.ownerSessionId || run.ownerSessionId === currentSessionId);
76746
+ }
76674
76747
  function setupRenderLoop(pi, ctx, extensionCtx, loadedConfig) {
76675
76748
  ctx.disposeRenderSchedulerSubscriptions();
76676
76749
  ctx.renderScheduler?.dispose();
@@ -76764,11 +76837,8 @@ function setupRenderLoop(pi, ctx, extensionCtx, loadedConfig) {
76764
76837
  ctx.widgetState.notificationCount ?? 0,
76765
76838
  manifests
76766
76839
  );
76767
- const currentSessionGen = ctx.sessionGeneration;
76768
- const currentSessionId = ctx.currentCtx ? ctx.currentCtx.sessionId : void 0;
76769
- const sessionManifests = manifests.filter(
76770
- (run) => !run.ownerSessionId || run.ownerSessionId === currentSessionId || run.ownerSessionGeneration === currentSessionGen
76771
- );
76840
+ const currentSessionId = ctx.currentCtx?.sessionManager?.getSessionId();
76841
+ const sessionManifests = filterManifestsForHealthNotifications(manifests, currentSessionId);
76772
76842
  const now = Date.now();
76773
76843
  for (const run of sessionManifests) {
76774
76844
  if (run.status !== "running") continue;
@@ -76998,6 +77068,7 @@ init_powerbar_publisher();
76998
77068
  init_widget();
76999
77069
  init_internal_error();
77000
77070
  init_paths();
77071
+ init_session_utils();
77001
77072
  init_async_notifier();
77002
77073
  init_team_tool2();
77003
77074
  init_lifecycle();
@@ -77021,6 +77092,7 @@ function buildCleanupSessionResourcesOnly(ctx) {
77021
77092
  return () => {
77022
77093
  if (ctx.cleanedUp) return;
77023
77094
  ctx.cleanedUp = true;
77095
+ const sid = extractSessionId(ctx.currentCtx);
77024
77096
  if (ctx.preloadTimer) {
77025
77097
  clearTimeout(ctx.preloadTimer);
77026
77098
  ctx.preloadTimer = void 0;
@@ -77032,7 +77104,7 @@ function buildCleanupSessionResourcesOnly(ctx) {
77032
77104
  ctx.stopSessionBoundSubagents();
77033
77105
  ctx.crewScheduler?.stop();
77034
77106
  stopAsyncRunNotifier(ctx.notifierState);
77035
- ctx.purgeStaleActiveRunIndexSyncIfLoaded();
77107
+ ctx.purgeStaleActiveRunIndexSyncIfLoaded(sid);
77036
77108
  stopCrewWidget(ctx.currentCtx, ctx.widgetState, ctx.currentCtx ? loadConfig(ctx.currentCtx.cwd).config.ui : void 0);
77037
77109
  clearPiCrewPowerbar(ctx.pi.events);
77038
77110
  disposePowerbarCoalescer();
@@ -77064,6 +77136,7 @@ function buildCleanupRuntime(ctx) {
77064
77136
  return () => {
77065
77137
  if (ctx.cleanedUp) return;
77066
77138
  ctx.cleanedUp = true;
77139
+ const sid = extractSessionId(ctx.currentCtx);
77067
77140
  if (ctx.preloadTimer) {
77068
77141
  clearTimeout(ctx.preloadTimer);
77069
77142
  ctx.preloadTimer = void 0;
@@ -77078,7 +77151,7 @@ function buildCleanupRuntime(ctx) {
77078
77151
  stopAllWatchdogs();
77079
77152
  ctx.crewScheduler?.stop();
77080
77153
  stopAsyncRunNotifier(ctx.notifierState);
77081
- ctx.purgeStaleActiveRunIndexSyncIfLoaded();
77154
+ ctx.purgeStaleActiveRunIndexSyncIfLoaded(sid);
77082
77155
  stopCrewWidget(ctx.currentCtx, ctx.widgetState, ctx.currentCtx ? loadConfig(ctx.currentCtx.cwd).config.ui : void 0);
77083
77156
  clearPiCrewPowerbar(ctx.pi.events);
77084
77157
  disposePowerbarCoalescer();
@@ -77155,7 +77228,7 @@ function createCompletionCoalescer(pi, ctx) {
77155
77228
  const f = ctx.subagentManager.getRecord(c.agentId);
77156
77229
  const p = ctx.currentCtx ? readPersistedSubagentRecord(ctx.currentCtx.cwd, c.agentId) : void 0;
77157
77230
  if (f?.resultConsumed || p?.resultConsumed) return false;
77158
- if (!ctx.isOwnerSessionCurrent(f?.ownerSessionGeneration ?? c.ownerGen)) return false;
77231
+ if (!ctx.isOwnerSessionCurrent(f?.ownerSessionGeneration ?? c.ownerGen, f?.ownerSessionId)) return false;
77159
77232
  return true;
77160
77233
  };
77161
77234
  const flush = () => {
@@ -77263,7 +77336,7 @@ function onTerminalStatus(pi, ctx, record, coalescer) {
77263
77336
  });
77264
77337
  }
77265
77338
  if (!record.background) return;
77266
- if (!ctx.isOwnerSessionCurrent(record.ownerSessionGeneration)) return;
77339
+ if (!ctx.isOwnerSessionCurrent(record.ownerSessionGeneration, record.ownerSessionId)) return;
77267
77340
  if (record.status !== "completed" && record.status !== "failed" && record.status !== "cancelled" && record.status !== "blocked" && record.status !== "error")
77268
77341
  return;
77269
77342
  const agentId = record.id;
@@ -77279,7 +77352,7 @@ function onTerminalStatus(pi, ctx, record, coalescer) {
77279
77352
  const fresh = ctx.subagentManager.getRecord(agentId);
77280
77353
  const persisted = ctx.currentCtx ? readPersistedSubagentRecord(ctx.currentCtx.cwd, agentId) : void 0;
77281
77354
  if (fresh?.resultConsumed || persisted?.resultConsumed) return;
77282
- if (!ctx.isOwnerSessionCurrent(fresh?.ownerSessionGeneration ?? ownerGen)) return;
77355
+ if (!ctx.isOwnerSessionCurrent(fresh?.ownerSessionGeneration ?? ownerGen, fresh?.ownerSessionId)) return;
77283
77356
  const member = {
77284
77357
  id: agentId,
77285
77358
  description: agentDescription,
@@ -77314,7 +77387,8 @@ function onTerminalStatus(pi, ctx, record, coalescer) {
77314
77387
  }
77315
77388
  function onInternalEvent(pi, ctx, event, payload) {
77316
77389
  const ownerGeneration = typeof payload?.ownerSessionGeneration === "number" ? payload.ownerSessionGeneration : void 0;
77317
- if (ownerGeneration !== void 0 && !ctx.isOwnerSessionCurrent(ownerGeneration)) return;
77390
+ const ownerSessionId = typeof payload?.ownerSessionId === "string" ? payload.ownerSessionId : void 0;
77391
+ if (ownerGeneration !== void 0 && !ctx.isOwnerSessionCurrent(ownerGeneration, ownerSessionId)) return;
77318
77392
  if (event === "subagent.stuck-blocked") {
77319
77393
  const p = payload;
77320
77394
  const id = typeof p.id === "string" ? p.id : "unknown";
@@ -77428,6 +77502,7 @@ function registerSubagentTools(pi, subagentManager, options = {}) {
77428
77502
  spawnOptions.ownerSessionGeneration = options.ownerSessionGeneration?.();
77429
77503
  if (!spawnOptions.prompt.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
77430
77504
  const ctxWithSession = withSessionId(ctx);
77505
+ spawnOptions.ownerSessionId = ctxWithSession.sessionId;
77431
77506
  const runner = async (currentOptions, childSignal) => handleTeamTool6(
77432
77507
  {
77433
77508
  action: "run",
@@ -77549,6 +77624,10 @@ function registerSubagentTools(pi, subagentManager, options = {}) {
77549
77624
  const inMemory = subagentManager.getRecord(p.agent_id);
77550
77625
  const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
77551
77626
  if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id }), {}, true);
77627
+ const currentSessionId = withSessionId(ctx).sessionId;
77628
+ if (record.ownerSessionId && record.ownerSessionId !== currentSessionId) {
77629
+ return subagentToolResult("Agent belongs to another session.", {}, true);
77630
+ }
77552
77631
  let current = refreshPersistedSubagentRecord(ctx, record);
77553
77632
  if (inMemory && current !== inMemory) Object.assign(inMemory, current);
77554
77633
  if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
@@ -77599,9 +77678,11 @@ function registerSubagentTools(pi, subagentManager, options = {}) {
77599
77678
  }
77600
77679
  const output = readSubagentRunResult(ctx, current);
77601
77680
  if (current.status !== "running" && current.status !== "queued" && current.status !== "blocked") {
77602
- current.resultConsumed = true;
77603
- if (inMemory) inMemory.resultConsumed = true;
77604
- savePersistedSubagentRecord(ctx.cwd, current);
77681
+ if (!current.ownerSessionId || current.ownerSessionId === currentSessionId) {
77682
+ current.resultConsumed = true;
77683
+ if (inMemory) inMemory.resultConsumed = true;
77684
+ savePersistedSubagentRecord(ctx.cwd, current);
77685
+ }
77605
77686
  }
77606
77687
  const text = [
77607
77688
  p.verbose ? formatSubagentRecord(current) : void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.9.58",
3
+ "version": "0.9.59",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",