@bastani/atomic 0.9.9-alpha.2 → 0.9.9-alpha.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 +10 -0
- package/dist/builtin/cursor/CHANGELOG.md +6 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +7 -0
- package/dist/builtin/intercom/README.md +10 -3
- package/dist/builtin/intercom/broker/client.ts +2 -0
- package/dist/builtin/intercom/broker/send-handler.ts +22 -20
- package/dist/builtin/intercom/foreground-detach-handoff.ts +3 -1
- package/dist/builtin/intercom/inbound-idle-queue.ts +82 -0
- package/dist/builtin/intercom/index-heavy.ts +47 -37
- package/dist/builtin/intercom/intercom-tool.ts +6 -4
- package/dist/builtin/intercom/intercom-utils.ts +30 -0
- package/dist/builtin/intercom/lifecycle.ts +8 -5
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/reply-tracker.ts +19 -19
- package/dist/builtin/intercom/session-target.ts +68 -0
- package/dist/builtin/intercom/skills/intercom/SKILL.md +7 -4
- package/dist/builtin/intercom/source-ownership.ts +34 -0
- package/dist/builtin/intercom/subagent-relay.ts +45 -20
- package/dist/builtin/intercom/terminal-ordering-barrier.ts +111 -0
- package/dist/builtin/intercom/types.ts +5 -0
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/runs/background/notify.ts +53 -9
- package/dist/builtin/subagents/src/shared/types-config.ts +1 -0
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +6 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/workflow-tool.ts +18 -2
- package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +9 -0
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-message-queue.d.ts +4 -1
- package/dist/core/agent-session-message-queue.d.ts.map +1 -1
- package/dist/core/agent-session-message-queue.js +35 -0
- package/dist/core/agent-session-message-queue.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +2 -1
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/extensions/api-types.d.ts +3 -1
- package/dist/core/extensions/api-types.d.ts.map +1 -1
- package/dist/core/extensions/api-types.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader-api.d.ts.map +1 -1
- package/dist/core/extensions/loader-api.js +4 -0
- package/dist/core/extensions/loader-api.js.map +1 -1
- package/dist/core/extensions/loader-runtime.d.ts.map +1 -1
- package/dist/core/extensions/loader-runtime.js +1 -0
- package/dist/core/extensions/loader-runtime.js.map +1 -1
- package/dist/core/extensions/message-types.d.ts +3 -0
- package/dist/core/extensions/message-types.d.ts.map +1 -1
- package/dist/core/extensions/message-types.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/runtime-types.d.ts +3 -1
- package/dist/core/extensions/runtime-types.d.ts.map +1 -1
- package/dist/core/extensions/runtime-types.js.map +1 -1
- package/docs/extensions.md +13 -0
- package/docs/subagents.md +2 -0
- package/docs/workflows.md +2 -2
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message, SessionInfo } from "./types.ts";
|
|
2
|
+
import { resolveSessionTarget, sessionTargetFailureReason } from "./session-target.js";
|
|
2
3
|
|
|
3
4
|
export interface IntercomContext {
|
|
4
5
|
from: SessionInfo;
|
|
@@ -6,14 +7,6 @@ export interface IntercomContext {
|
|
|
6
7
|
receivedAt: number;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
function matchesPendingSender(context: IntercomContext, to: string): boolean {
|
|
10
|
-
if (context.from.id === to) {
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return context.from.name?.toLowerCase() === to.toLowerCase();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
10
|
export class ReplyTracker {
|
|
18
11
|
private readonly pendingAsks = new Map<string, IntercomContext>();
|
|
19
12
|
private readonly pendingTurnContexts: IntercomContext[] = [];
|
|
@@ -56,21 +49,28 @@ export class ReplyTracker {
|
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
const pending = Array.from(this.pendingAsks.values());
|
|
59
|
-
if (pending.length === 1) {
|
|
60
|
-
return pending[0]!;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
52
|
if (options.to) {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
53
|
+
const senders = [...new Map(
|
|
54
|
+
pending.map((context) => [context.from.id, context.from] as const),
|
|
55
|
+
).values()];
|
|
56
|
+
const resolution = resolveSessionTarget(senders, options.to);
|
|
57
|
+
if (resolution.kind !== "resolved") {
|
|
58
|
+
if (resolution.kind === "not_found") {
|
|
59
|
+
throw new Error(`No pending ask from "${options.to}"`);
|
|
60
|
+
}
|
|
61
|
+
throw new Error(sessionTargetFailureReason(options.to, resolution));
|
|
67
62
|
}
|
|
63
|
+
const matches = pending.filter(
|
|
64
|
+
(context) => context.from.id === resolution.session.id,
|
|
65
|
+
);
|
|
66
|
+
if (matches.length === 1) return matches[0]!;
|
|
68
67
|
if (matches.length > 1) {
|
|
69
|
-
throw new Error(`Multiple pending asks from
|
|
70
|
-
}
|
|
71
|
-
if (pending.length > 1) {
|
|
72
|
-
throw new Error(`No pending ask from \"${options.to}\"`);
|
|
68
|
+
throw new Error(`Multiple pending asks from "${options.to}"`);
|
|
73
69
|
}
|
|
70
|
+
throw new Error(`No pending ask from "${options.to}"`);
|
|
71
|
+
}
|
|
72
|
+
if (pending.length === 1) {
|
|
73
|
+
return pending[0]!;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (pending.length === 0) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { SessionInfo } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type SessionTargetResolution =
|
|
4
|
+
| { kind: "resolved"; session: SessionInfo }
|
|
5
|
+
| { kind: "ambiguous_name"; matches: readonly SessionInfo[] }
|
|
6
|
+
| { kind: "ambiguous_prefix"; matches: readonly SessionInfo[] }
|
|
7
|
+
| { kind: "not_found" };
|
|
8
|
+
|
|
9
|
+
/** Resolve an Intercom session by exact ID, exact case-insensitive name, or unique ID prefix. */
|
|
10
|
+
export function resolveSessionTarget(
|
|
11
|
+
sessions: readonly SessionInfo[],
|
|
12
|
+
nameOrId: string,
|
|
13
|
+
): SessionTargetResolution {
|
|
14
|
+
const target = nameOrId.trim();
|
|
15
|
+
const exactId = sessions.find((session) => session.id === target);
|
|
16
|
+
if (exactId !== undefined) return { kind: "resolved", session: exactId };
|
|
17
|
+
|
|
18
|
+
const lowerTarget = target.toLowerCase();
|
|
19
|
+
const exactNames = sessions.filter(
|
|
20
|
+
(session) => session.name?.toLowerCase() === lowerTarget,
|
|
21
|
+
);
|
|
22
|
+
if (exactNames.length === 1) {
|
|
23
|
+
return { kind: "resolved", session: exactNames[0]! };
|
|
24
|
+
}
|
|
25
|
+
if (exactNames.length > 1) {
|
|
26
|
+
return { kind: "ambiguous_name", matches: exactNames };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const prefixedIds = sessions.filter((session) => session.id.startsWith(target));
|
|
30
|
+
if (prefixedIds.length === 1) {
|
|
31
|
+
return { kind: "resolved", session: prefixedIds[0]! };
|
|
32
|
+
}
|
|
33
|
+
if (prefixedIds.length > 1) {
|
|
34
|
+
return { kind: "ambiguous_prefix", matches: prefixedIds };
|
|
35
|
+
}
|
|
36
|
+
return { kind: "not_found" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function sessionTargetFailureReason(
|
|
40
|
+
target: string,
|
|
41
|
+
resolution: Exclude<SessionTargetResolution, { kind: "resolved" }>,
|
|
42
|
+
): string {
|
|
43
|
+
if (resolution.kind === "ambiguous_name") {
|
|
44
|
+
return `Multiple sessions named "${target}" are connected. Use the session ID instead.`;
|
|
45
|
+
}
|
|
46
|
+
if (resolution.kind === "ambiguous_prefix") {
|
|
47
|
+
const labels = resolution.matches
|
|
48
|
+
.map((session) => `${session.name ?? "Unnamed session"} (${session.id})`)
|
|
49
|
+
.join(", ");
|
|
50
|
+
return `Ambiguous session ID prefix "${target}" matches: ${labels}. Use a longer ID or an exact session name.`;
|
|
51
|
+
}
|
|
52
|
+
return "Session not found";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SessionListingClient {
|
|
56
|
+
listSessions(): Promise<SessionInfo[]>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Resolve a target through the broker's current session list. */
|
|
60
|
+
export async function resolveSessionTargetId(
|
|
61
|
+
client: SessionListingClient,
|
|
62
|
+
nameOrId: string,
|
|
63
|
+
): Promise<string | null> {
|
|
64
|
+
const resolution = resolveSessionTarget(await client.listSessions(), nameOrId);
|
|
65
|
+
if (resolution.kind === "resolved") return resolution.session.id;
|
|
66
|
+
if (resolution.kind === "not_found") return null;
|
|
67
|
+
throw new Error(sessionTargetFailureReason(nameOrId, resolution));
|
|
68
|
+
}
|
|
@@ -67,13 +67,16 @@ intercom({
|
|
|
67
67
|
|
|
68
68
|
### Pattern 2: Quick Status Check
|
|
69
69
|
|
|
70
|
-
Before sending, verify who's connected
|
|
70
|
+
Before sending, verify who's connected. The short ID printed by `list` is directly usable by `send`, `ask`, and targeted `reply`:
|
|
71
71
|
|
|
72
72
|
```typescript
|
|
73
73
|
intercom({ action: "list" })
|
|
74
|
-
// →
|
|
74
|
+
// → • planner (6332faab) — /workspace (model) [idle]
|
|
75
|
+
intercom({ action: "ask", to: "6332faab", message: "Which option should I use?" })
|
|
75
76
|
```
|
|
76
77
|
|
|
78
|
+
Intercom resolves exact full IDs first, exact case-insensitive names second, and unique ID prefixes last. A colliding prefix returns an ambiguity error; use a longer displayed ID or an exact name rather than guessing.
|
|
79
|
+
|
|
77
80
|
### Pattern 3: Reply Naturally
|
|
78
81
|
|
|
79
82
|
When responding to an inbound ask, prefer `reply` instead of reconstructing raw IDs:
|
|
@@ -199,9 +202,9 @@ message, treat it as a `contact_supervisor` escalation.
|
|
|
199
202
|
|--------|----------|----------|
|
|
200
203
|
| `send` | Fire-and-forget | You don't need a response |
|
|
201
204
|
| `ask` | Blocks until reply (10 min timeout) | You need an answer to continue |
|
|
202
|
-
| `reply` | Responds to the active or pending inbound ask | You were asked something and need to answer naturally |
|
|
205
|
+
| `reply` | Responds to the active or pending inbound ask; `to` accepts a displayed short ID | You were asked something and need to answer naturally |
|
|
203
206
|
| `pending` | Lists unresolved inbound asks | You need to see who is waiting before replying |
|
|
204
|
-
| `list` | Returns all sessions with live status | You need to discover targets or choose an idle peer |
|
|
207
|
+
| `list` | Returns all sessions with actionable short IDs and live status | You need to discover targets or choose an idle peer |
|
|
205
208
|
| `status` | Returns your connection state | Troubleshooting |
|
|
206
209
|
|
|
207
210
|
## Optional: Visible Peer Sessions via cmux, tmux, or psmux (Windows rewrite of tmux that has fully parity with tmux)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { APP_NAME, getEnvValue } from "@bastani/atomic";
|
|
2
|
+
import type { Message } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const ENV_PREFIX = APP_NAME.toUpperCase();
|
|
5
|
+
const SUBAGENT_RUN_ID_ENV = `${ENV_PREFIX}_SUBAGENT_RUN_ID`;
|
|
6
|
+
const SUBAGENT_CHILD_AGENT_ENV = `${ENV_PREFIX}_SUBAGENT_CHILD_AGENT`;
|
|
7
|
+
const SUBAGENT_CHILD_INDEX_ENV = `${ENV_PREFIX}_SUBAGENT_CHILD_INDEX`;
|
|
8
|
+
|
|
9
|
+
export function buildSubagentMessageSource(
|
|
10
|
+
runIdValue: string | undefined,
|
|
11
|
+
agentValue: string | undefined,
|
|
12
|
+
indexValue: string | undefined,
|
|
13
|
+
): Message["source"] | undefined {
|
|
14
|
+
const subagentRunId = runIdValue?.trim();
|
|
15
|
+
if (!subagentRunId) return undefined;
|
|
16
|
+
const subagentAgent = agentValue?.trim();
|
|
17
|
+
const rawIndex = indexValue?.trim();
|
|
18
|
+
const parsedIndex = rawIndex === undefined ? undefined : Number(rawIndex);
|
|
19
|
+
const subagentIndex = parsedIndex !== undefined && Number.isInteger(parsedIndex) && parsedIndex >= 0
|
|
20
|
+
? parsedIndex : undefined;
|
|
21
|
+
return {
|
|
22
|
+
subagentRunId,
|
|
23
|
+
...(subagentAgent ? { subagentAgent } : {}),
|
|
24
|
+
...(subagentIndex !== undefined ? { subagentIndex } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readSubagentMessageSource(): Message["source"] | undefined {
|
|
29
|
+
return buildSubagentMessageSource(
|
|
30
|
+
getEnvValue(SUBAGENT_RUN_ID_ENV),
|
|
31
|
+
getEnvValue(SUBAGENT_CHILD_AGENT_ENV),
|
|
32
|
+
getEnvValue(SUBAGENT_CHILD_INDEX_ENV),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -6,11 +6,14 @@ import {
|
|
|
6
6
|
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
7
7
|
SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
|
|
8
8
|
SUBAGENT_RESULT_INTERCOM_EVENT,
|
|
9
|
+
SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT,
|
|
9
10
|
getErrorMessage,
|
|
10
11
|
parseSubagentIntercomPayload,
|
|
12
|
+
parseSubagentResultBarrier,
|
|
11
13
|
} from "./intercom-utils.js";
|
|
12
14
|
import { DeliveredMessageCache } from "./broker/delivered-message-cache.js";
|
|
13
15
|
import { buildSendSignature } from "./broker/send-signature.js";
|
|
16
|
+
import { emitGlobalTerminalOrderingBarrier } from "./terminal-ordering-barrier.js";
|
|
14
17
|
|
|
15
18
|
interface SubagentRelayDeps {
|
|
16
19
|
runtimeGeneration(): number;
|
|
@@ -26,26 +29,47 @@ interface SubagentRelayDeps {
|
|
|
26
29
|
export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps): void {
|
|
27
30
|
const { getLiveContext, currentSessionTargetMatches, sendIncomingMessage, ensureConnected, resolveSessionTarget } = deps;
|
|
28
31
|
const localDeliveries = new DeliveredMessageCache();
|
|
29
|
-
function deliverLocalSubagentRelayMessage(
|
|
32
|
+
function deliverLocalSubagentRelayMessage(
|
|
33
|
+
sender: "subagent-control" | "subagent-result",
|
|
34
|
+
status: string,
|
|
35
|
+
messageText: string,
|
|
36
|
+
terminalBarrier?: { runId: string; terminalId?: string; sourceSessionTargets: string[] },
|
|
37
|
+
): void {
|
|
30
38
|
const now = Date.now();
|
|
31
|
-
|
|
39
|
+
const entry = {
|
|
32
40
|
from: {
|
|
33
|
-
id: sender,
|
|
34
|
-
|
|
35
|
-
cwd: deps.runtimeContext()?.cwd ?? process.cwd(),
|
|
36
|
-
model: sender,
|
|
37
|
-
pid: process.pid,
|
|
38
|
-
startedAt: now,
|
|
39
|
-
lastActivity: now,
|
|
40
|
-
status,
|
|
41
|
-
},
|
|
42
|
-
message: {
|
|
43
|
-
id: randomUUID(),
|
|
44
|
-
timestamp: now,
|
|
45
|
-
content: { text: messageText },
|
|
41
|
+
id: sender, name: sender, cwd: deps.runtimeContext()?.cwd ?? process.cwd(),
|
|
42
|
+
model: sender, pid: process.pid, startedAt: now, lastActivity: now, status,
|
|
46
43
|
},
|
|
44
|
+
message: { id: randomUUID(), timestamp: now, content: { text: messageText } },
|
|
47
45
|
bodyText: messageText,
|
|
48
|
-
}
|
|
46
|
+
};
|
|
47
|
+
let dispatched = false;
|
|
48
|
+
if (terminalBarrier) {
|
|
49
|
+
const payload = {
|
|
50
|
+
...terminalBarrier,
|
|
51
|
+
terminalAt: now,
|
|
52
|
+
source: "result-relay" as const,
|
|
53
|
+
dispatch: (prefix: Array<{ customType: string; content: string; display: boolean; details?: unknown }>) => {
|
|
54
|
+
const terminalMessage = {
|
|
55
|
+
customType: "intercom_message",
|
|
56
|
+
content: `**📨 From ${sender}** (${entry.from.cwd})\n\n${messageText}`,
|
|
57
|
+
display: true,
|
|
58
|
+
details: entry,
|
|
59
|
+
};
|
|
60
|
+
if (typeof pi.sendMessages === "function") {
|
|
61
|
+
pi.sendMessages([...prefix, terminalMessage], { triggerTurn: true });
|
|
62
|
+
} else {
|
|
63
|
+
for (const message of prefix) pi.sendMessage(message, { deliverAs: "steer" });
|
|
64
|
+
pi.sendMessage(terminalMessage, { triggerTurn: true });
|
|
65
|
+
}
|
|
66
|
+
dispatched = true;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
pi.events.emit(SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT, payload);
|
|
70
|
+
emitGlobalTerminalOrderingBarrier(payload);
|
|
71
|
+
}
|
|
72
|
+
if (!dispatched) sendIncomingMessage(entry, "trigger");
|
|
49
73
|
}
|
|
50
74
|
function recordSubagentDeliveryError(entryType: string, to: string, message: string, error: unknown): void {
|
|
51
75
|
pi.appendEntry(entryType, {
|
|
@@ -68,7 +92,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
|
|
|
68
92
|
}
|
|
69
93
|
function deliverLocal(
|
|
70
94
|
parsed: ReturnType<typeof parseSubagentIntercomPayload> & {},
|
|
71
|
-
options: { sender: "subagent-control" | "subagent-result"; status: string; errorEntryType: string; acknowledge?: boolean },
|
|
95
|
+
options: { sender: "subagent-control" | "subagent-result"; status: string; errorEntryType: string; acknowledge?: boolean; terminalBarrier?: { runId: string; terminalId?: string; sourceSessionTargets: string[] } },
|
|
72
96
|
): void {
|
|
73
97
|
try {
|
|
74
98
|
const signature = buildSendSignature(parsed.to, { text: parsed.message });
|
|
@@ -77,7 +101,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
|
|
|
77
101
|
throw new Error(`Intercom message ID '${parsed.requestId}' was already delivered with a different target or payload`);
|
|
78
102
|
}
|
|
79
103
|
if (match === "miss") {
|
|
80
|
-
deliverLocalSubagentRelayMessage(options.sender, options.status, parsed.message);
|
|
104
|
+
deliverLocalSubagentRelayMessage(options.sender, options.status, parsed.message, options.terminalBarrier);
|
|
81
105
|
if (parsed.requestId) localDeliveries.record(parsed.requestId, signature);
|
|
82
106
|
}
|
|
83
107
|
acknowledgeResult(options, parsed.requestId, true);
|
|
@@ -102,6 +126,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
|
|
|
102
126
|
}): void {
|
|
103
127
|
const parsed = parseSubagentIntercomPayload(payload);
|
|
104
128
|
if (!parsed) return;
|
|
129
|
+
const terminalBarrier = options.sender === "subagent-result" ? parseSubagentResultBarrier(payload) ?? undefined : undefined;
|
|
105
130
|
|
|
106
131
|
const relayGeneration = deps.runtimeGeneration();
|
|
107
132
|
void (async () => {
|
|
@@ -112,7 +137,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
|
|
|
112
137
|
return;
|
|
113
138
|
}
|
|
114
139
|
if (currentSessionTargetMatches(parsed.to)) {
|
|
115
|
-
deliverLocal(parsed, options);
|
|
140
|
+
deliverLocal(parsed, { ...options, terminalBarrier });
|
|
116
141
|
return;
|
|
117
142
|
}
|
|
118
143
|
if (!deps.runtimeStarted()) {
|
|
@@ -145,7 +170,7 @@ export function registerSubagentRelay(pi: ExtensionAPI, deps: SubagentRelayDeps)
|
|
|
145
170
|
return;
|
|
146
171
|
}
|
|
147
172
|
if (currentSessionTargetMatches(parsed.to, target, activeClient)) {
|
|
148
|
-
deliverLocal(parsed, options);
|
|
173
|
+
deliverLocal(parsed, { ...options, terminalBarrier });
|
|
149
174
|
return;
|
|
150
175
|
}
|
|
151
176
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@bastani/atomic";
|
|
2
|
+
import { SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT, type InboundMessageEntry } from "./intercom-utils.js";
|
|
3
|
+
import type { InboundIdleQueue } from "./inbound-idle-queue.js";
|
|
4
|
+
|
|
5
|
+
export { SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT } from "./intercom-utils.js";
|
|
6
|
+
|
|
7
|
+
const GLOBAL_BARRIER_HANDLER = "__atomicTerminalOrderingBarrierHandler";
|
|
8
|
+
|
|
9
|
+
export interface OrderedTerminalPreludeMessage {
|
|
10
|
+
customType: "intercom_message";
|
|
11
|
+
content: string;
|
|
12
|
+
display: true;
|
|
13
|
+
details: InboundMessageEntry;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TerminalOrderingBarrier {
|
|
17
|
+
runId: string;
|
|
18
|
+
terminalId: string;
|
|
19
|
+
terminalAt: number;
|
|
20
|
+
source: "background-notify" | "result-relay";
|
|
21
|
+
sourceSessionTargets: string[];
|
|
22
|
+
dispatch?(prefix: OrderedTerminalPreludeMessage[]): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TerminalOrderingBarrierOptions {
|
|
26
|
+
queue: InboundIdleQueue;
|
|
27
|
+
toMessage?(entry: InboundMessageEntry): OrderedTerminalPreludeMessage;
|
|
28
|
+
deliver(entry: InboundMessageEntry, mode: "prelude"): void;
|
|
29
|
+
onDrain?(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseBarrier(value: unknown): TerminalOrderingBarrier | null {
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
34
|
+
const record = value as Record<string, unknown>;
|
|
35
|
+
if (typeof record.runId !== "string" || !record.runId) return null;
|
|
36
|
+
if (typeof record.terminalAt !== "number" || !Number.isFinite(record.terminalAt)) return null;
|
|
37
|
+
if (record.source !== "background-notify" && record.source !== "result-relay") return null;
|
|
38
|
+
if (!Array.isArray(record.sourceSessionTargets)) return null;
|
|
39
|
+
const sourceSessionTargets = record.sourceSessionTargets
|
|
40
|
+
.filter((target): target is string => typeof target === "string")
|
|
41
|
+
.map((target) => target.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
if (sourceSessionTargets.length === 0) return null;
|
|
44
|
+
const terminalId = typeof record.terminalId === "string" && record.terminalId
|
|
45
|
+
? record.terminalId : `${record.source}:${record.terminalAt}`;
|
|
46
|
+
return {
|
|
47
|
+
runId: record.runId,
|
|
48
|
+
terminalId,
|
|
49
|
+
terminalAt: record.terminalAt,
|
|
50
|
+
source: record.source,
|
|
51
|
+
sourceSessionTargets,
|
|
52
|
+
...(typeof record.dispatch === "function"
|
|
53
|
+
? { dispatch: record.dispatch as (prefix: OrderedTerminalPreludeMessage[]) => void }
|
|
54
|
+
: {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function emitGlobalTerminalOrderingBarrier(value: unknown): void {
|
|
59
|
+
const handler = (globalThis as Record<string, unknown>)[GLOBAL_BARRIER_HANDLER];
|
|
60
|
+
if (typeof handler === "function") (handler as (payload: unknown) => void)(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Claims accepted same-child messages before terminal delivery. */
|
|
64
|
+
export function registerTerminalOrderingBarrier(
|
|
65
|
+
pi: Pick<ExtensionAPI, "events">,
|
|
66
|
+
options: TerminalOrderingBarrierOptions,
|
|
67
|
+
): () => void {
|
|
68
|
+
const drainedTargetsByTerminal = new Map<string, Set<string>>();
|
|
69
|
+
const handle = (value: unknown): void => {
|
|
70
|
+
const barrier = parseBarrier(value);
|
|
71
|
+
if (!barrier) return;
|
|
72
|
+
const terminalKey = `${barrier.runId}\0${barrier.terminalId}`;
|
|
73
|
+
const drainedTargets = drainedTargetsByTerminal.get(terminalKey) ?? new Set<string>();
|
|
74
|
+
const pendingTargets = barrier.sourceSessionTargets.filter((target) => !drainedTargets.has(target));
|
|
75
|
+
if (pendingTargets.length === 0) return;
|
|
76
|
+
const claim = options.queue.claimOrdinarySourceTargets(barrier.runId, pendingTargets, barrier.terminalAt);
|
|
77
|
+
if (barrier.dispatch && options.toMessage) {
|
|
78
|
+
try {
|
|
79
|
+
barrier.dispatch(claim.entries.map(options.toMessage));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
claim.rollbackFrom(0);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
for (let index = 0; index < claim.entries.length; index++) {
|
|
86
|
+
try {
|
|
87
|
+
options.deliver(claim.entries[index]!, "prelude");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
claim.rollbackFrom(index);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if ((barrier.dispatch && options.toMessage) || claim.entries.length > 0) {
|
|
95
|
+
for (const target of pendingTargets) drainedTargets.add(target);
|
|
96
|
+
drainedTargetsByTerminal.set(terminalKey, drainedTargets);
|
|
97
|
+
}
|
|
98
|
+
if (drainedTargetsByTerminal.size > 1_000) {
|
|
99
|
+
const oldestKey = drainedTargetsByTerminal.keys().next().value;
|
|
100
|
+
if (oldestKey !== undefined) drainedTargetsByTerminal.delete(oldestKey);
|
|
101
|
+
}
|
|
102
|
+
if (claim.entries.length > 0) options.onDrain?.();
|
|
103
|
+
};
|
|
104
|
+
(globalThis as Record<string, unknown>)[GLOBAL_BARRIER_HANDLER] = handle;
|
|
105
|
+
const unsubscribe = pi.events.on(SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT, handle);
|
|
106
|
+
return () => {
|
|
107
|
+
unsubscribe();
|
|
108
|
+
const globals = globalThis as Record<string, unknown>;
|
|
109
|
+
if (globals[GLOBAL_BARRIER_HANDLER] === handle) delete globals[GLOBAL_BARRIER_HANDLER];
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -14,6 +14,11 @@ export interface Message {
|
|
|
14
14
|
timestamp: number;
|
|
15
15
|
replyTo?: string;
|
|
16
16
|
expectsReply?: boolean;
|
|
17
|
+
source?: {
|
|
18
|
+
subagentRunId: string;
|
|
19
|
+
subagentAgent?: string;
|
|
20
|
+
subagentIndex?: number;
|
|
21
|
+
};
|
|
17
22
|
content: {
|
|
18
23
|
text: string;
|
|
19
24
|
attachments?: Attachment[];
|
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.9-alpha.3] - 2026-07-14
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Published a synchronized Atomic 0.9.9-alpha.3 prerelease for the MCP extension; no functional MCP changes were made after 0.9.9-alpha.2.
|
|
15
|
+
|
|
10
16
|
## [0.9.9-alpha.2] - 2026-07-14
|
|
11
17
|
|
|
12
18
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/mcp",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
|
|
6
6
|
"contributors": [
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.9-alpha.3] - 2026-07-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Preserved background async-child completion chronology with Intercom by emitting a terminal-ordering barrier and atomically batching claimed same-child messages before the completion notice, with a compatibility fallback for hosts without batched message admission.
|
|
10
|
+
|
|
5
11
|
## [0.9.9-alpha.2] - 2026-07-14
|
|
6
12
|
|
|
7
13
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/subagents",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.3",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and background runs. Fork of: https://github.com/nicobailon/pi-subagents",
|
|
6
6
|
"contributors": [
|
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@bastani/atomic";
|
|
6
|
+
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
6
7
|
import { buildCompletionKey, hasSeenWithTtl, recordSeen } from "./completion-dedupe.ts";
|
|
7
8
|
import type { CompletionNotificationEnvelope } from "./completion-notification.ts";
|
|
8
|
-
import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
|
|
9
|
+
import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT } from "../../shared/types.ts";
|
|
9
10
|
|
|
10
11
|
interface ChainStepResult {
|
|
11
12
|
agent: string;
|
|
12
13
|
output: string;
|
|
13
14
|
success: boolean;
|
|
15
|
+
intercomTarget?: string;
|
|
16
|
+
index?: number;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface SubagentNotifyDetails {
|
|
@@ -25,6 +28,8 @@ export interface SubagentNotifyDetails {
|
|
|
25
28
|
|
|
26
29
|
interface SubagentResult {
|
|
27
30
|
id: string | null;
|
|
31
|
+
runId?: string;
|
|
32
|
+
notificationId?: string;
|
|
28
33
|
agent: string | null;
|
|
29
34
|
success: boolean;
|
|
30
35
|
summary: string;
|
|
@@ -41,6 +46,13 @@ interface SubagentResult {
|
|
|
41
46
|
totalTasks?: number;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
interface TerminalPreludeMessage {
|
|
50
|
+
customType: string;
|
|
51
|
+
content: string;
|
|
52
|
+
display: boolean;
|
|
53
|
+
details?: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
interface NotifyRegistration {
|
|
45
57
|
unsubscribe: () => void;
|
|
46
58
|
}
|
|
@@ -87,6 +99,30 @@ export default function registerSubagentNotify(pi: ExtensionAPI): () => void {
|
|
|
87
99
|
const seen = getNotifySeen(pi);
|
|
88
100
|
const ttlMs = 10 * 60 * 1000;
|
|
89
101
|
|
|
102
|
+
const emitTerminalOrderingBarrier = (result: SubagentResult, dispatch?: (prefix: TerminalPreludeMessage[]) => void): void => {
|
|
103
|
+
const runId = result.runId ?? result.id;
|
|
104
|
+
if (!runId) return;
|
|
105
|
+
const resultTargets = result.results?.map((child, arrayIndex) =>
|
|
106
|
+
child.intercomTarget?.trim() || resolveSubagentIntercomTarget(runId, child.agent, child.index ?? arrayIndex)) ?? [];
|
|
107
|
+
const sourceSessionTargets = resultTargets.length > 0
|
|
108
|
+
? resultTargets
|
|
109
|
+
: result.agent ? [resolveSubagentIntercomTarget(runId, result.agent, 0)] : [];
|
|
110
|
+
if (sourceSessionTargets.length === 0) return;
|
|
111
|
+
const terminalId = result.notificationId?.startsWith("completion-notify-")
|
|
112
|
+
? result.notificationId.slice("completion-notify-".length)
|
|
113
|
+
: result.notificationId;
|
|
114
|
+
const payload = {
|
|
115
|
+
runId,
|
|
116
|
+
...(terminalId ? { terminalId } : {}),
|
|
117
|
+
terminalAt: Number.isFinite(result.timestamp) ? result.timestamp : Date.now(),
|
|
118
|
+
source: "background-notify" as const,
|
|
119
|
+
sourceSessionTargets,
|
|
120
|
+
...(dispatch ? { dispatch } : {}),
|
|
121
|
+
};
|
|
122
|
+
pi.events.emit(SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT, payload);
|
|
123
|
+
const globalHandler = (globalThis as Record<string, unknown>).__atomicTerminalOrderingBarrierHandler;
|
|
124
|
+
if (typeof globalHandler === "function") (globalHandler as (value: unknown) => void)(payload);
|
|
125
|
+
};
|
|
90
126
|
let registration: NotifyRegistration;
|
|
91
127
|
const handleComplete = (data: unknown) => {
|
|
92
128
|
if (registry.get(pi) !== registration) return;
|
|
@@ -130,15 +166,23 @@ export default function registerSubagentNotify(pi: ExtensionAPI): () => void {
|
|
|
130
166
|
.filter((line) => line !== undefined)
|
|
131
167
|
.join("\n");
|
|
132
168
|
|
|
169
|
+
const terminalMessage = {
|
|
170
|
+
customType: "subagent-notify",
|
|
171
|
+
content,
|
|
172
|
+
display: true,
|
|
173
|
+
};
|
|
133
174
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
175
|
+
let dispatched = false;
|
|
176
|
+
emitTerminalOrderingBarrier(result, (prefix) => {
|
|
177
|
+
if (typeof pi.sendMessages === "function") {
|
|
178
|
+
pi.sendMessages([...prefix, terminalMessage], { triggerTurn: true });
|
|
179
|
+
} else {
|
|
180
|
+
for (const message of prefix) pi.sendMessage(message, { deliverAs: "steer" });
|
|
181
|
+
pi.sendMessage(terminalMessage, { triggerTurn: true });
|
|
182
|
+
}
|
|
183
|
+
dispatched = true;
|
|
184
|
+
});
|
|
185
|
+
if (!dispatched) pi.sendMessage(terminalMessage, { triggerTurn: true });
|
|
142
186
|
recordSeen(seen, key, Date.now());
|
|
143
187
|
result.acknowledge?.(true);
|
|
144
188
|
} catch (error) {
|
|
@@ -48,6 +48,7 @@ export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
|
|
|
48
48
|
export const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
|
|
49
49
|
export const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
|
|
50
50
|
export const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
|
|
51
|
+
export const SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT = "subagent:terminal-ordering-barrier";
|
|
51
52
|
export const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
|
|
52
53
|
|
|
53
54
|
// ============================================================================
|
|
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.9-alpha.3] - 2026-07-14
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Published a synchronized Atomic 0.9.9-alpha.3 prerelease for the web-access extension; no functional web-access changes were made after 0.9.9-alpha.2.
|
|
12
|
+
|
|
7
13
|
## [0.9.9-alpha.2] - 2026-07-14
|
|
8
14
|
|
|
9
15
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.3",
|
|
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": [
|
|
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.9-alpha.3] - 2026-07-14
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Made `workflow({ action: "status", runId })` apply the same exact-or-unique-prefix contract as the other run actions. A prefix shared by multiple retained runs now returns the standard ambiguity diagnostic instead of silently inspecting the first match, so abbreviated run IDs printed by status surfaces remain safe and actionable.
|
|
14
|
+
|
|
9
15
|
## [0.9.9-alpha.2] - 2026-07-14
|
|
10
16
|
|
|
11
17
|
### Changed
|