@bastani/atomic 0.9.47 → 0.9.48-alpha.1

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.
@@ -4,6 +4,13 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.48-alpha.1] - 2026-10-27
8
+
9
+ ### Fixed
10
+
11
+ - A session restored after Atomic restarts now rejoins the Intercom groups it had joined, so it can still reach its restored subagents in those groups without joining again. Each `join` and `leave` is saved with the session, and a restored session rejoins exactly the groups its own last record lists before it first connects. A group it left before the restart is not rejoined, a forked session does not inherit the record, and a session restored under a different home group keeps only its home group. The same holds for a resumed workflow stage, in process or on a replacement local worker: it comes back in its invocation group or subgroup and in every group it joined, so the launcher reaches it by path and its teammates by name ([#4079](https://github.com/bastani-inc/atomic/issues/4079))
12
+ - A send to a session that is not in any of the sender's groups, but is live in a group the sender's read-only `intercom list` peek can show, now fails with `<name> is live in group <group>; join it to send` instead of `Session not found`. Workflow groups keep their own rules: a workflow stage never gets the hint, no sender is told to join a `workflow:` group, and a session visible only in a group the peek refuses still gets `Session not found` ([#4079](https://github.com/bastani-inc/atomic/issues/4079))
13
+
7
14
  ## [0.9.46] - 2026-10-27
8
15
 
9
16
  ### Fixed
@@ -191,6 +191,11 @@ startup identity. An ordinary host can therefore control several joined workflow
191
191
  invocations, while a workflow worker cannot gain another invocation's parent-control
192
192
  authority by joining its group and reconnecting.
193
193
 
194
+ Joined memberships are also saved with the session: a session restored after
195
+ Atomic restarts rejoins the groups its last `join`/`leave` left it in, and a group
196
+ it left is not rejoined. A `send` to a session that is live only in a group your
197
+ read-only `list` peek can show fails with `<name> is live in group <group>; join it to send`.
198
+
194
199
  **Worker finds something unexpected — escalates and waits:**
195
200
  ```typescript
196
201
  intercom({
@@ -2259,6 +2259,7 @@ class IntercomBroker {
2259
2259
  this.holdForRestoringRecipient,
2260
2260
  this.receipts,
2261
2261
  this.routeRemote,
2262
+ (sender, group) => this.canInspectSelectedGroup(sender, group),
2262
2263
  );
2263
2264
  }
2264
2265
 
@@ -169,6 +169,8 @@ export function handleBrokerSend(
169
169
  holdForRestoringRecipient?: RestoringRecipientHold,
170
170
  receipts?: RecipientReceipts<BrokerConnectedSession>,
171
171
  routeRemote?: RemoteSendRouter,
172
+ /** #4079: whether the sender's read-only `list` peek may inspect a group; names a hidden target's group. */
173
+ canPeekGroup?: (sender: BrokerConnectedSession, group: string) => boolean,
172
174
  ): void {
173
175
  const message = clientMessage.message;
174
176
  const messageId = wireMessageId(message);
@@ -406,6 +408,8 @@ export function handleBrokerSend(
406
408
  isNonAgentWorkflowTarget,
407
409
  holdForRestoringRecipient,
408
410
  receipts,
411
+ undefined,
412
+ canPeekGroup,
409
413
  ),
410
414
  });
411
415
  return;
@@ -473,6 +477,7 @@ export function handleBrokerSend(
473
477
  holdForRestoringRecipient,
474
478
  receipts,
475
479
  routeRemote,
480
+ canPeekGroup,
476
481
  );
477
482
  },
478
483
  });
@@ -820,13 +825,47 @@ export function handleBrokerSend(
820
825
  ) {
821
826
  return;
822
827
  }
828
+ const peekHint =
829
+ resolution.kind === "not_found" && !supervisorSend && !requirePendingReply && expectedRecipientId === undefined &&
830
+ message.replyTo === undefined && workflowTarget === undefined && canPeekGroup !== undefined
831
+ ? liveInPeekableGroupReason(sessions, fromSession, trimmedTo, canPeekGroup)
832
+ : undefined;
823
833
  write(socket, {
824
834
  type: "delivery_failed",
825
835
  messageId: message.id,
826
836
  attemptId,
827
- reason: sessionTargetFailureReason(clientMessage.to, resolution),
837
+ reason: peekHint ?? sessionTargetFailureReason(clientMessage.to, resolution),
828
838
  ...(requirePendingReply && resolution.kind === "not_found"
829
839
  ? { reasonCode: "session_not_found" as const }
830
840
  : {}),
831
841
  });
832
842
  }
843
+
844
+ /**
845
+ * #4079: a target outside every group the sender shares that is live in a group the sender's read-only `list`
846
+ * peek already shows. Names one such group so the sender knows to join it. A target visible only in groups the
847
+ * peek refuses, or matching more than one peekable session, gets no hint, so this reveals nothing `list` hides.
848
+ * Workflow groups keep their own ownership rules: a workflow stage is never told to join a group outside its run,
849
+ * and no sender is told to join a `workflow:` group, since joining one is how a host takes control of a run.
850
+ */
851
+ function liveInPeekableGroupReason(
852
+ sessions: Map<string, BrokerConnectedSession>,
853
+ sender: BrokerConnectedSession,
854
+ target: string,
855
+ canPeekGroup: (sender: BrokerConnectedSession, group: string) => boolean,
856
+ ): string | undefined {
857
+ if (normalizeGroup(sender.registrationGroup ?? sender.info.group).startsWith("workflow:")) return undefined;
858
+ const peekable = new Map<string, string>();
859
+ for (const { info } of sessions.values()) {
860
+ if (info.id === sender.info.id || !isAgentRecipient(info) || sessionsShareGroup(info, sender.info)) continue;
861
+ const group = [...sessionGroups(info)].find(
862
+ (candidate) => !candidate.startsWith("workflow:") && canPeekGroup(sender, candidate),
863
+ );
864
+ if (group !== undefined) peekable.set(info.id, group);
865
+ }
866
+ const candidates = Array.from(peekable.keys(), (id) => sessions.get(id)!.info);
867
+ const resolution = resolveSessionTarget(candidates, target);
868
+ if (resolution.kind !== "resolved") return undefined;
869
+ const group = peekable.get(resolution.session.id)!;
870
+ return `${resolution.session.name ?? resolution.session.id} is live in group ${group}; join it to send`;
871
+ }
@@ -552,6 +552,26 @@ function readIntercomSessionIdentity(entries, sessionId) {
552
552
  }
553
553
  var INTERCOM_SESSION_IDENTITY_ENTRY = "intercom_session_identity";
554
554
 
555
+ // dist/builtin/intercom/session-memberships.ts
556
+ function isMemberships(record) {
557
+ return record !== undefined && record !== null && typeof record.sessionId === "string" && typeof record.homeGroup === "string" && (record.groups === null || Array.isArray(record.groups) && record.groups.every((group) => typeof group === "string"));
558
+ }
559
+ function readIntercomSessionMemberships(entries, sessionId, homeGroup) {
560
+ for (let index = entries.length - 1;index >= 0; index -= 1) {
561
+ const entry = entries[index];
562
+ if (entry?.type !== "custom" || entry.customType !== INTERCOM_SESSION_MEMBERSHIPS_ENTRY)
563
+ continue;
564
+ const data = entry.data;
565
+ if (!isMemberships(data) || data.sessionId !== sessionId)
566
+ continue;
567
+ if (data.homeGroup !== homeGroup || data.groups === null)
568
+ return;
569
+ return data.groups;
570
+ }
571
+ return;
572
+ }
573
+ var INTERCOM_SESSION_MEMBERSHIPS_ENTRY = "intercom_session_memberships";
574
+
555
575
  // dist/builtin/intercom/broker/framing.ts
556
576
  function writeMessage(socket, msg, callback) {
557
577
  const json = JSON.stringify(msg);
@@ -4363,7 +4383,7 @@ one shared membership; contact_supervisor remains the only cross-group path.`,
4363
4383
  }
4364
4384
  try {
4365
4385
  const groups = await connectedClient.joinGroup(targetGroup);
4366
- deps.setJoinedGroups(groups);
4386
+ await deps.setJoinedGroups(groups);
4367
4387
  return {
4368
4388
  content: [{ type: "text", text: `Joined intercom group "${targetGroup}". Memberships: ${groups.join(", ")}.` }],
4369
4389
  isError: false,
@@ -4398,10 +4418,10 @@ one shared membership; contact_supervisor remains the only cross-group path.`,
4398
4418
  throw new Error("Intercom home group is unavailable");
4399
4419
  await connectedClient.updatePresenceAcked({ groups: [homeGroup] });
4400
4420
  groups = connectedClient.groups;
4401
- deps.clearJoinedGroups();
4421
+ await deps.clearJoinedGroups();
4402
4422
  } else {
4403
4423
  groups = await connectedClient.leaveGroup(targetGroup);
4404
- deps.setJoinedGroups(groups);
4424
+ await deps.setJoinedGroups(groups);
4405
4425
  }
4406
4426
  const text = targetGroup === undefined ? `Returned to home intercom membership. Memberships: ${groups.join(", ")}.` : `Left intercom group "${targetGroup}". Memberships: ${groups.join(", ")}.`;
4407
4427
  return {
@@ -7873,6 +7893,25 @@ function piIntercomExtension(pi, testOverrides = {}) {
7873
7893
  sessionMemberships.delete(currentSessionId);
7874
7894
  releaseJoinedGroups();
7875
7895
  }
7896
+ async function persistJoinedGroups(groups) {
7897
+ const sessionId = currentSessionId;
7898
+ if (!sessionId)
7899
+ return;
7900
+ try {
7901
+ await pi.appendEntry(INTERCOM_SESSION_MEMBERSHIPS_ENTRY, {
7902
+ sessionId,
7903
+ homeGroup: resolveSessionHomeGroup(),
7904
+ groups: groups === null ? null : [...normalizeGroups(groups)]
7905
+ });
7906
+ } catch {}
7907
+ }
7908
+ function persistedJoinedGroups(ctx) {
7909
+ try {
7910
+ return readIntercomSessionMemberships(ctx.sessionManager.getEntries(), ctx.sessionManager.getSessionId(), resolveSessionHomeGroup());
7911
+ } catch {
7912
+ return;
7913
+ }
7914
+ }
7876
7915
  function releaseJoinedGroups() {
7877
7916
  joinedGroups = null;
7878
7917
  const sessionId = currentSessionId;
@@ -8930,7 +8969,7 @@ function piIntercomExtension(pi, testOverrides = {}) {
8930
8969
  currentStatus
8931
8970
  });
8932
8971
  pi.on("session_start", async (event, ctx) => {
8933
- const retainedMemberships = event.reason === "reload" ? sessionMemberships.get(ctx.sessionManager.getSessionId()) : undefined;
8972
+ const retainedMemberships = event.reason === "reload" ? sessionMemberships.get(ctx.sessionManager.getSessionId()) : persistedJoinedGroups(ctx);
8934
8973
  if (retainedMemberships)
8935
8974
  setJoinedGroups(retainedMemberships);
8936
8975
  const pendingStageDelivery = ctx.orchestrationContext?.kind === "workflow-stage" ? ctx.orchestrationContext.pendingStageDelivery : undefined;
@@ -8990,8 +9029,14 @@ function piIntercomExtension(pi, testOverrides = {}) {
8990
9029
  beginReplyWait: (from, replyTo, signal) => replyWaiters.begin(from, replyTo, signal),
8991
9030
  confirmSend: config.confirmSend,
8992
9031
  homeGroup: resolveSessionHomeGroup,
8993
- setJoinedGroups,
8994
- clearJoinedGroups,
9032
+ setJoinedGroups: async (groups) => {
9033
+ setJoinedGroups(groups);
9034
+ await persistJoinedGroups(groups);
9035
+ },
9036
+ clearJoinedGroups: async () => {
9037
+ clearJoinedGroups();
9038
+ await persistJoinedGroups(null);
9039
+ },
8995
9040
  replyTracker: () => replyTracker
8996
9041
  });
8997
9042
  registerIntercomOverlay(pi, {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.47",
3
+ "version": "0.9.48-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.47",
3
+ "version": "0.9.48-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic's pi-compatible MCP integration for server connections, OAuth, tools, resources, and terminal management.",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.47",
3
+ "version": "0.9.48-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.47",
3
+ "version": "0.9.48-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.47",
3
+ "version": "0.9.48-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.9.47",
3
+ "version": "0.9.48-alpha.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bastani/atomic",
9
- "version": "0.9.47",
9
+ "version": "0.9.48-alpha.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "@bastani/atomic-natives": "0.9.47",
13
- "@bastani/pi-ai": "0.9.47",
12
+ "@bastani/atomic-natives": "0.9.48-alpha.1",
13
+ "@bastani/pi-ai": "0.9.48-alpha.1",
14
14
  "@earendil-works/pi-agent-core": "1.4.0",
15
15
  "@earendil-works/pi-codemode": "1.4.0",
16
16
  "@earendil-works/pi-tui": "1.4.0",
@@ -427,18 +427,18 @@
427
427
  }
428
428
  },
429
429
  "node_modules/@bastani/atomic-natives": {
430
- "version": "0.9.47",
431
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.47.tgz",
430
+ "version": "0.9.48-alpha.1",
431
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.48-alpha.1.tgz",
432
432
  "license": "MIT",
433
433
  "optionalDependencies": {
434
- "@bastani/atomic-natives-darwin-arm64": "0.9.47",
435
- "@bastani/atomic-natives-darwin-x64": "0.9.47",
436
- "@bastani/atomic-natives-linux-arm64-gnu": "0.9.47",
437
- "@bastani/atomic-natives-linux-arm64-musl": "0.9.47",
438
- "@bastani/atomic-natives-linux-x64-gnu": "0.9.47",
439
- "@bastani/atomic-natives-linux-x64-musl": "0.9.47",
440
- "@bastani/atomic-natives-win32-arm64-msvc": "0.9.47",
441
- "@bastani/atomic-natives-win32-x64-msvc": "0.9.47"
434
+ "@bastani/atomic-natives-darwin-arm64": "0.9.48-alpha.1",
435
+ "@bastani/atomic-natives-darwin-x64": "0.9.48-alpha.1",
436
+ "@bastani/atomic-natives-linux-arm64-gnu": "0.9.48-alpha.1",
437
+ "@bastani/atomic-natives-linux-arm64-musl": "0.9.48-alpha.1",
438
+ "@bastani/atomic-natives-linux-x64-gnu": "0.9.48-alpha.1",
439
+ "@bastani/atomic-natives-linux-x64-musl": "0.9.48-alpha.1",
440
+ "@bastani/atomic-natives-win32-arm64-msvc": "0.9.48-alpha.1",
441
+ "@bastani/atomic-natives-win32-x64-msvc": "0.9.48-alpha.1"
442
442
  },
443
443
  "engines": {
444
444
  "bun": ">=1.4.2",
@@ -446,8 +446,8 @@
446
446
  }
447
447
  },
448
448
  "node_modules/@bastani/atomic-natives-darwin-arm64": {
449
- "version": "0.9.47",
450
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.47.tgz",
449
+ "version": "0.9.48-alpha.1",
450
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.48-alpha.1.tgz",
451
451
  "license": "MIT",
452
452
  "os": [
453
453
  "darwin"
@@ -458,8 +458,8 @@
458
458
  "optional": true
459
459
  },
460
460
  "node_modules/@bastani/atomic-natives-darwin-x64": {
461
- "version": "0.9.47",
462
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.47.tgz",
461
+ "version": "0.9.48-alpha.1",
462
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.48-alpha.1.tgz",
463
463
  "license": "MIT",
464
464
  "os": [
465
465
  "darwin"
@@ -470,8 +470,8 @@
470
470
  "optional": true
471
471
  },
472
472
  "node_modules/@bastani/atomic-natives-linux-arm64-gnu": {
473
- "version": "0.9.47",
474
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.47.tgz",
473
+ "version": "0.9.48-alpha.1",
474
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.48-alpha.1.tgz",
475
475
  "license": "MIT",
476
476
  "os": [
477
477
  "linux"
@@ -485,8 +485,8 @@
485
485
  "optional": true
486
486
  },
487
487
  "node_modules/@bastani/atomic-natives-linux-arm64-musl": {
488
- "version": "0.9.47",
489
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-musl/-/atomic-natives-linux-arm64-musl-0.9.47.tgz",
488
+ "version": "0.9.48-alpha.1",
489
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-musl/-/atomic-natives-linux-arm64-musl-0.9.48-alpha.1.tgz",
490
490
  "license": "MIT",
491
491
  "os": [
492
492
  "linux"
@@ -500,8 +500,8 @@
500
500
  "optional": true
501
501
  },
502
502
  "node_modules/@bastani/atomic-natives-linux-x64-gnu": {
503
- "version": "0.9.47",
504
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.47.tgz",
503
+ "version": "0.9.48-alpha.1",
504
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.48-alpha.1.tgz",
505
505
  "license": "MIT",
506
506
  "os": [
507
507
  "linux"
@@ -515,8 +515,8 @@
515
515
  "optional": true
516
516
  },
517
517
  "node_modules/@bastani/atomic-natives-linux-x64-musl": {
518
- "version": "0.9.47",
519
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-musl/-/atomic-natives-linux-x64-musl-0.9.47.tgz",
518
+ "version": "0.9.48-alpha.1",
519
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-musl/-/atomic-natives-linux-x64-musl-0.9.48-alpha.1.tgz",
520
520
  "license": "MIT",
521
521
  "os": [
522
522
  "linux"
@@ -530,8 +530,8 @@
530
530
  "optional": true
531
531
  },
532
532
  "node_modules/@bastani/atomic-natives-win32-arm64-msvc": {
533
- "version": "0.9.47",
534
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.47.tgz",
533
+ "version": "0.9.48-alpha.1",
534
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.48-alpha.1.tgz",
535
535
  "license": "MIT",
536
536
  "os": [
537
537
  "win32"
@@ -542,8 +542,8 @@
542
542
  "optional": true
543
543
  },
544
544
  "node_modules/@bastani/atomic-natives-win32-x64-msvc": {
545
- "version": "0.9.47",
546
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.47.tgz",
545
+ "version": "0.9.48-alpha.1",
546
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.48-alpha.1.tgz",
547
547
  "license": "MIT",
548
548
  "os": [
549
549
  "win32"
@@ -554,8 +554,8 @@
554
554
  "optional": true
555
555
  },
556
556
  "node_modules/@bastani/pi-ai": {
557
- "version": "0.9.47",
558
- "resolved": "https://registry.npmjs.org/@bastani/pi-ai/-/pi-ai-0.9.47.tgz",
557
+ "version": "0.9.48-alpha.1",
558
+ "resolved": "https://registry.npmjs.org/@bastani/pi-ai/-/pi-ai-0.9.48-alpha.1.tgz",
559
559
  "license": "MIT",
560
560
  "dependencies": {
561
561
  "@anthropic-ai/sdk": "0.129.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.9.47",
3
+ "version": "0.9.48-alpha.1",
4
4
  "description": "Atomic coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "atomicConfig": {
@@ -89,8 +89,8 @@
89
89
  "prepublishOnly": "bun run clean && bun run build && bun run shrinkwrap"
90
90
  },
91
91
  "dependencies": {
92
- "@bastani/atomic-natives": "0.9.47",
93
- "@bastani/pi-ai": "0.9.47",
92
+ "@bastani/atomic-natives": "0.9.48-alpha.1",
93
+ "@bastani/pi-ai": "0.9.48-alpha.1",
94
94
  "@earendil-works/pi-agent-core": "1.4.0",
95
95
  "@earendil-works/pi-codemode": "1.4.0",
96
96
  "@earendil-works/pi-tui": "1.4.0",