@beeos-ai/device-agent 0.8.0 → 0.9.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.
Files changed (79) hide show
  1. package/dist/app/fixed-session-id.d.ts +19 -0
  2. package/dist/app/fixed-session-id.js +26 -0
  3. package/dist/app/fixed-session-id.js.map +1 -0
  4. package/dist/app/jsonl-session-sweeper.d.ts +90 -0
  5. package/dist/app/jsonl-session-sweeper.js +162 -0
  6. package/dist/app/jsonl-session-sweeper.js.map +1 -0
  7. package/dist/app/jsonl-store.d.ts +10 -0
  8. package/dist/app/jsonl-store.js +10 -4
  9. package/dist/app/jsonl-store.js.map +1 -1
  10. package/dist/app/session-manager.d.ts +135 -1
  11. package/dist/app/session-manager.js +195 -12
  12. package/dist/app/session-manager.js.map +1 -1
  13. package/dist/bootstrap.js +146 -2
  14. package/dist/bootstrap.js.map +1 -1
  15. package/dist/config/loader.js +2 -0
  16. package/dist/config/loader.js.map +1 -1
  17. package/dist/config/schema.d.ts +2 -0
  18. package/dist/config/schema.js +41 -1
  19. package/dist/config/schema.js.map +1 -1
  20. package/dist/fleet/domain/device.d.ts +17 -0
  21. package/dist/fleet/domain/device.js +1 -0
  22. package/dist/fleet/domain/device.js.map +1 -1
  23. package/dist/fleet/domain/fsm.d.ts +30 -1
  24. package/dist/fleet/domain/fsm.js +100 -4
  25. package/dist/fleet/domain/fsm.js.map +1 -1
  26. package/dist/fleet/supervisor.js +38 -18
  27. package/dist/fleet/supervisor.js.map +1 -1
  28. package/dist/plugins/acp/index.d.ts +27 -30
  29. package/dist/plugins/acp/index.js +44 -137
  30. package/dist/plugins/acp/index.js.map +1 -1
  31. package/dist/plugins/message-bus/busy-reply.js +13 -5
  32. package/dist/plugins/message-bus/busy-reply.js.map +1 -1
  33. package/dist/plugins/message-bus/cancel.d.ts +48 -0
  34. package/dist/plugins/message-bus/cancel.js +66 -0
  35. package/dist/plugins/message-bus/cancel.js.map +1 -0
  36. package/dist/plugins/message-bus/inbound.d.ts +38 -22
  37. package/dist/plugins/message-bus/inbound.js +113 -50
  38. package/dist/plugins/message-bus/inbound.js.map +1 -1
  39. package/dist/plugins/message-bus/index.d.ts +1 -2
  40. package/dist/plugins/message-bus/index.js +39 -37
  41. package/dist/plugins/message-bus/index.js.map +1 -1
  42. package/dist/plugins/message-bus/ms-channel-notifier.d.ts +113 -0
  43. package/dist/plugins/message-bus/ms-channel-notifier.js +206 -0
  44. package/dist/plugins/message-bus/ms-channel-notifier.js.map +1 -0
  45. package/dist/plugins/message-bus/ms-inflight-tracker.d.ts +36 -0
  46. package/dist/plugins/message-bus/ms-inflight-tracker.js +42 -0
  47. package/dist/plugins/message-bus/ms-inflight-tracker.js.map +1 -0
  48. package/dist/ports/plugin.d.ts +12 -0
  49. package/package.json +2 -2
  50. package/dist/agent-runtime/skills/sqlite-store.d.ts +0 -60
  51. package/dist/agent-runtime/skills/sqlite-store.js +0 -234
  52. package/dist/agent-runtime/skills/sqlite-store.js.map +0 -1
  53. package/dist/plugins/message-bus/outbound.d.ts +0 -43
  54. package/dist/plugins/message-bus/outbound.js +0 -149
  55. package/dist/plugins/message-bus/outbound.js.map +0 -1
  56. package/dist/plugins/message-bus/turn-context.d.ts +0 -57
  57. package/dist/plugins/message-bus/turn-context.js +0 -53
  58. package/dist/plugins/message-bus/turn-context.js.map +0 -1
  59. package/dist/test-lan-autoglm.d.ts +0 -7
  60. package/dist/test-lan-autoglm.js +0 -46
  61. package/dist/test-lan-autoglm.js.map +0 -1
  62. package/dist/test-mem-breakdown.d.ts +0 -1
  63. package/dist/test-mem-breakdown.js +0 -103
  64. package/dist/test-mem-breakdown.js.map +0 -1
  65. package/dist/test-mem.d.ts +0 -1
  66. package/dist/test-mem.js +0 -60
  67. package/dist/test-mem.js.map +0 -1
  68. package/dist/test-mobile-use.d.ts +0 -1
  69. package/dist/test-mobile-use.js +0 -18
  70. package/dist/test-mobile-use.js.map +0 -1
  71. package/dist/test-screenshot-loop.d.ts +0 -1
  72. package/dist/test-screenshot-loop.js +0 -43
  73. package/dist/test-screenshot-loop.js.map +0 -1
  74. package/dist/test-screenshot.d.ts +0 -1
  75. package/dist/test-screenshot.js +0 -10
  76. package/dist/test-screenshot.js.map +0 -1
  77. package/dist/test-ui-xml-dump.d.ts +0 -15
  78. package/dist/test-ui-xml-dump.js +0 -49
  79. package/dist/test-ui-xml-dump.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"busy-reply.js","sourceRoot":"","sources":["../../../src/plugins/message-bus/busy-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAwBH,MAAM,OAAO,0BAA0B;IACR;IAA7B,YAA6B,IAA6B;QAA7B,SAAI,GAAJ,IAAI,CAAyB;IAAG,CAAC;IAE9D,KAAK,CAAC,SAAS,CAAC,GAAqB;QACnC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAC/B,GAAG,CAAC,SAAS,EACb,aAAa,EACb;YACE,WAAW,EAAE,GAAG,CAAC,UAAU;YAC3B,KAAK,EAAE,YAAY;YACnB,IAAI,EAAE,8EAA8E;SACrF,EACD,EAAE,SAAS,EAAE,GAAG,CAAC,gBAAgB,EAAE,CACpC,CAAC;IACJ,CAAC;CACF"}
1
+ {"version":3,"file":"busy-reply.js","sourceRoot":"","sources":["../../../src/plugins/message-bus/busy-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAwBH,MAAM,OAAO,0BAA0B;IACR;IAA7B,YAA6B,IAA6B;QAA7B,SAAI,GAAJ,IAAI,CAAyB;IAAG,CAAC;IAE9D,KAAK,CAAC,SAAS,CAAC,GAAqB;QACnC,oEAAoE;QACpE,mEAAmE;QACnE,mEAAmE;QACnE,2DAA2D;QAC3D,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,SAAS,EAAE;YACpD,IAAI,EAAE,aAAa;YACnB,OAAO,EAAE;gBACP,WAAW,EAAE,GAAG,CAAC,UAAU;gBAC3B,KAAK,EAAE,YAAY;gBACnB,IAAI,EAAE,8EAA8E;aACrF;YACD,SAAS,EAAE,GAAG,CAAC,gBAAgB;SAChC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Inbound — `chat_cancel` envelope handler.
3
+ *
4
+ * The BeeOS platform pushes a protocol-agnostic `chat_cancel` envelope
5
+ * onto the agent's personal Centrifugo channel when an in-flight
6
+ * `chat_message` turn must be aborted. Triggers include:
7
+ *
8
+ * - A2A Service `tasks/cancel` JSON-RPC (caller-initiated)
9
+ * - A2A Service `InvokeTask` ctx-cancel fallback (caller HTTP
10
+ * disconnect / deadline)
11
+ * - A2A Service timeout sweeper (task exceeded `timeout_ms`)
12
+ *
13
+ * The wire payload (see `backend/pkg/convmsg/extract.go`
14
+ * `MessageTypeChatCancel`) carries IM primitives only:
15
+ *
16
+ * {
17
+ * "target_message_id": "<message_id of the original chat_message>",
18
+ * "reason": "canceled" | "caller_disconnected" | "timeout"
19
+ * }
20
+ *
21
+ * Agents MUST NOT decode protocol headers (no `task_id`, no
22
+ * `a2a_task_cancel`) — that contract is enforced here by intentionally
23
+ * reading nothing besides `target_message_id` and `reason`.
24
+ *
25
+ * v0.9.0 lookup path: query the tiny `MsInflightTracker` for the
26
+ * matching sid, then call `sessionManager.cancel()`. The SessionManager
27
+ * itself owns the AbortController and only allows ONE in-flight turn
28
+ * globally — so resolving by inboundMessageId is unambiguous (either
29
+ * the current turn is THIS turn, or the cancel is for a stale /
30
+ * already-completed turn we should silently drop).
31
+ *
32
+ * No-ops:
33
+ * - missing / non-string `target_message_id` → drop, debug log
34
+ * - tracker has no entry for that message_id → drop, debug log
35
+ * (the turn already finished, or this is a stray cancel for a
36
+ * different agent on a shared principal — both safe to ignore)
37
+ */
38
+ import type { MessageEnvelope } from "@beeos-ai/message-sdk";
39
+ import type { SessionManager } from "../../app/session-manager.js";
40
+ import type { Logger } from "../../logger/index.js";
41
+ import type { MsInflightTracker } from "./ms-inflight-tracker.js";
42
+ export declare const MESSAGE_TYPE_CHAT_CANCEL = "chat_cancel";
43
+ export interface MessageBusCancelHandlerOptions {
44
+ sessionManager: SessionManager;
45
+ tracker: MsInflightTracker;
46
+ logger: Logger;
47
+ }
48
+ export declare function buildMessageBusCancelHandler(opts: MessageBusCancelHandlerOptions): (envelope: MessageEnvelope) => Promise<void>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Inbound — `chat_cancel` envelope handler.
3
+ *
4
+ * The BeeOS platform pushes a protocol-agnostic `chat_cancel` envelope
5
+ * onto the agent's personal Centrifugo channel when an in-flight
6
+ * `chat_message` turn must be aborted. Triggers include:
7
+ *
8
+ * - A2A Service `tasks/cancel` JSON-RPC (caller-initiated)
9
+ * - A2A Service `InvokeTask` ctx-cancel fallback (caller HTTP
10
+ * disconnect / deadline)
11
+ * - A2A Service timeout sweeper (task exceeded `timeout_ms`)
12
+ *
13
+ * The wire payload (see `backend/pkg/convmsg/extract.go`
14
+ * `MessageTypeChatCancel`) carries IM primitives only:
15
+ *
16
+ * {
17
+ * "target_message_id": "<message_id of the original chat_message>",
18
+ * "reason": "canceled" | "caller_disconnected" | "timeout"
19
+ * }
20
+ *
21
+ * Agents MUST NOT decode protocol headers (no `task_id`, no
22
+ * `a2a_task_cancel`) — that contract is enforced here by intentionally
23
+ * reading nothing besides `target_message_id` and `reason`.
24
+ *
25
+ * v0.9.0 lookup path: query the tiny `MsInflightTracker` for the
26
+ * matching sid, then call `sessionManager.cancel()`. The SessionManager
27
+ * itself owns the AbortController and only allows ONE in-flight turn
28
+ * globally — so resolving by inboundMessageId is unambiguous (either
29
+ * the current turn is THIS turn, or the cancel is for a stale /
30
+ * already-completed turn we should silently drop).
31
+ *
32
+ * No-ops:
33
+ * - missing / non-string `target_message_id` → drop, debug log
34
+ * - tracker has no entry for that message_id → drop, debug log
35
+ * (the turn already finished, or this is a stray cancel for a
36
+ * different agent on a shared principal — both safe to ignore)
37
+ */
38
+ export const MESSAGE_TYPE_CHAT_CANCEL = "chat_cancel";
39
+ export function buildMessageBusCancelHandler(opts) {
40
+ return async (envelope) => {
41
+ const payload = (envelope.payload ?? {});
42
+ const targetMessageId = pickString(payload.target_message_id);
43
+ if (!targetMessageId) {
44
+ opts.logger.debug({ type: envelope.type, message_id: envelope.message_id }, "message-bus: chat_cancel dropped (no target_message_id)");
45
+ return;
46
+ }
47
+ const reason = pickString(payload.reason) || "unknown";
48
+ const entry = opts.tracker.findByInboundMessageId(targetMessageId);
49
+ if (!entry) {
50
+ opts.logger.debug({ targetMessageId, reason }, "message-bus: chat_cancel for unknown / already-finished turn — no-op");
51
+ return;
52
+ }
53
+ const cancelled = await opts.sessionManager.cancel();
54
+ opts.logger.info({
55
+ sid: entry.sid,
56
+ channelId: entry.channelId,
57
+ targetMessageId,
58
+ reason,
59
+ cancelled,
60
+ }, "message-bus: chat_cancel received — aborting in-flight turn");
61
+ };
62
+ }
63
+ function pickString(v) {
64
+ return typeof v === "string" && v.length > 0 ? v : "";
65
+ }
66
+ //# sourceMappingURL=cancel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cancel.js","sourceRoot":"","sources":["../../../src/plugins/message-bus/cancel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AASH,MAAM,CAAC,MAAM,wBAAwB,GAAG,aAAa,CAAC;AAQtD,MAAM,UAAU,4BAA4B,CAC1C,IAAoC;IAEpC,OAAO,KAAK,EAAE,QAAQ,EAAE,EAAE;QACxB,MAAM,OAAO,GAAG,CAAC,QAAQ,CAAC,OAAO,IAAI,EAAE,CAA4B,CAAC;QACpE,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAC9D,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,EACxD,yDAAyD,CAC1D,CAAC;YACF,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC;QAEvD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,sBAAsB,CAAC,eAAe,CAAC,CAAC;QACnE,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,EAAE,eAAe,EAAE,MAAM,EAAE,EAC3B,sEAAsE,CACvE,CAAC;YACF,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;QACrD,IAAI,CAAC,MAAM,CAAC,IAAI,CACd;YACE,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,eAAe;YACf,MAAM;YACN,SAAS;SACV,EACD,6DAA6D,CAC9D,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,CAAU;IAC5B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC"}
@@ -1,37 +1,53 @@
1
1
  /**
2
2
  * Inbound — `chat_message` / `agent_request` envelope handler.
3
3
  *
4
- * Wires `MessageSDK.onMessage(...)` to `AgentHost.submitTask(...)`,
5
- * extracting the prompt + IM correlators via the shared
6
- * `extractChatPrompt` helper from `@beeos-ai/message-sdk`.
4
+ * v0.9.0 — wires `MessageSDK.onMessage(...)` directly to
5
+ * `SessionManager.promptForSession(sid, prompt)`. Per-channel session
6
+ * isolation:
7
7
  *
8
- * Flow per inbound envelope:
9
- *
10
- * 1. Extract `{ message, channelId, messageId, contextId }` from the
11
- * payload (returns null on malformed envelopes — silently dropped).
8
+ * 1. Extract `{ message, channelId, messageId, contextId, sessionKey,
9
+ * files }` from the payload via `extractChatPrompt` (returns null
10
+ * on malformed envelopes — silently dropped).
12
11
  * 2. Reject if no `channelId` or no `messageId` — replies must round
13
12
  * trip through `ch:{channelId}` with `in_reply_to=messageId`, so
14
- * neither is optional for our outbound bridge.
15
- * 3. Try to claim the in-flight turn register. If busy → busy-reply
16
- * path (handled by the caller); otherwise submit to the host.
17
- * 4. The submitted `taskId` gets stored in the register so the
18
- * outbound listener can correlate AgentEvents back to the right
19
- * MS channel.
13
+ * neither is optional.
14
+ * 3. Layer 3 in-flight gate: if `sessionManager.isTurnInFlight()`
15
+ * returns true, send a busy-reply on the inbound channel and
16
+ * return. The agent serialises every prompt through a single
17
+ * physical device, so concurrent requests are rejected (not
18
+ * queued) — matches v0.8.0 contract.
19
+ * 4. Resolve the per-channel sid from `sessionKey` (the SDK already
20
+ * computed `agent:{id}:invoke:ctx-{contextId}` /
21
+ * `:ch-{channelId}`). Bind a fresh `MsChannelNotifier` to that
22
+ * sid for the duration of the turn.
23
+ * 5. Build the prompt (text + a minimal `[attachments]` block when
24
+ * `files.length > 0`) and call
25
+ * `sessionManager.promptForSession(sid, promptText)`.
26
+ * 6. The notifier translates streaming session/update envelopes into
27
+ * MS wire envelopes (`agent_reply_delta`, terminal `agent_reply`)
28
+ * automatically — no separate outbound listener needed.
29
+ * 7. ALWAYS unbind the notifier on completion (success / error /
30
+ * cancellation) so the next turn on the same channel gets a fresh
31
+ * notifier with `accumulatedText = ""`.
20
32
  *
21
- * The inbound handler intentionally does NOT subscribe to the host
22
- * event stream — that's `outbound.ts`'s job, registered ONCE at plugin
23
- * start. Keeping the two sides decoupled means we never accidentally
24
- * double-publish for the same turn.
33
+ * Defensive paths:
34
+ * - If `promptForSession` rejects with `"agent_busy"` (extremely
35
+ * rare race after the explicit `isTurnInFlight` check), fall
36
+ * through to the busy-reply path so the caller's `/wait` doesn't
37
+ * hang.
38
+ * - For any other thrown error, publish a terminal `agent_reply`
39
+ * with `error` set so callers see a definite outcome.
25
40
  */
26
- import { type MessageEnvelope } from "@beeos-ai/message-sdk";
27
- import type { AgentHost } from "../../ports/agent-host.js";
41
+ import { type MessageEnvelope, type MessageSDK } from "@beeos-ai/message-sdk";
42
+ import type { SessionManager } from "../../app/session-manager.js";
28
43
  import type { Logger } from "../../logger/index.js";
29
- import type { TurnContextRegister } from "./turn-context.js";
30
44
  import type { BusyReplyHandler } from "./busy-reply.js";
45
+ import type { MsInflightTracker } from "./ms-inflight-tracker.js";
31
46
  export interface MessageBusInboundOptions {
32
- host: AgentHost;
33
- register: TurnContextRegister;
47
+ sessionManager: SessionManager;
48
+ sdk: MessageSDK;
34
49
  busyReply: BusyReplyHandler;
50
+ tracker: MsInflightTracker;
35
51
  logger: Logger;
36
52
  /**
37
53
  * Local agent identity surfaced to `extractChatPrompt`. Used only for
@@ -1,29 +1,45 @@
1
1
  /**
2
2
  * Inbound — `chat_message` / `agent_request` envelope handler.
3
3
  *
4
- * Wires `MessageSDK.onMessage(...)` to `AgentHost.submitTask(...)`,
5
- * extracting the prompt + IM correlators via the shared
6
- * `extractChatPrompt` helper from `@beeos-ai/message-sdk`.
4
+ * v0.9.0 — wires `MessageSDK.onMessage(...)` directly to
5
+ * `SessionManager.promptForSession(sid, prompt)`. Per-channel session
6
+ * isolation:
7
7
  *
8
- * Flow per inbound envelope:
9
- *
10
- * 1. Extract `{ message, channelId, messageId, contextId }` from the
11
- * payload (returns null on malformed envelopes — silently dropped).
8
+ * 1. Extract `{ message, channelId, messageId, contextId, sessionKey,
9
+ * files }` from the payload via `extractChatPrompt` (returns null
10
+ * on malformed envelopes — silently dropped).
12
11
  * 2. Reject if no `channelId` or no `messageId` — replies must round
13
12
  * trip through `ch:{channelId}` with `in_reply_to=messageId`, so
14
- * neither is optional for our outbound bridge.
15
- * 3. Try to claim the in-flight turn register. If busy → busy-reply
16
- * path (handled by the caller); otherwise submit to the host.
17
- * 4. The submitted `taskId` gets stored in the register so the
18
- * outbound listener can correlate AgentEvents back to the right
19
- * MS channel.
13
+ * neither is optional.
14
+ * 3. Layer 3 in-flight gate: if `sessionManager.isTurnInFlight()`
15
+ * returns true, send a busy-reply on the inbound channel and
16
+ * return. The agent serialises every prompt through a single
17
+ * physical device, so concurrent requests are rejected (not
18
+ * queued) — matches v0.8.0 contract.
19
+ * 4. Resolve the per-channel sid from `sessionKey` (the SDK already
20
+ * computed `agent:{id}:invoke:ctx-{contextId}` /
21
+ * `:ch-{channelId}`). Bind a fresh `MsChannelNotifier` to that
22
+ * sid for the duration of the turn.
23
+ * 5. Build the prompt (text + a minimal `[attachments]` block when
24
+ * `files.length > 0`) and call
25
+ * `sessionManager.promptForSession(sid, promptText)`.
26
+ * 6. The notifier translates streaming session/update envelopes into
27
+ * MS wire envelopes (`agent_reply_delta`, terminal `agent_reply`)
28
+ * automatically — no separate outbound listener needed.
29
+ * 7. ALWAYS unbind the notifier on completion (success / error /
30
+ * cancellation) so the next turn on the same channel gets a fresh
31
+ * notifier with `accumulatedText = ""`.
20
32
  *
21
- * The inbound handler intentionally does NOT subscribe to the host
22
- * event stream — that's `outbound.ts`'s job, registered ONCE at plugin
23
- * start. Keeping the two sides decoupled means we never accidentally
24
- * double-publish for the same turn.
33
+ * Defensive paths:
34
+ * - If `promptForSession` rejects with `"agent_busy"` (extremely
35
+ * rare race after the explicit `isTurnInFlight` check), fall
36
+ * through to the busy-reply path so the caller's `/wait` doesn't
37
+ * hang.
38
+ * - For any other thrown error, publish a terminal `agent_reply`
39
+ * with `error` set so callers see a definite outcome.
25
40
  */
26
41
  import { extractChatPrompt, } from "@beeos-ai/message-sdk";
42
+ import { MsChannelNotifier } from "./ms-channel-notifier.js";
27
43
  /**
28
44
  * Build the per-envelope handler. The returned function is what gets
29
45
  * registered with `sdk.onMessage(type, handler)` — keeping the factory
@@ -48,11 +64,14 @@ export function buildMessageBusInboundHandler(opts) {
48
64
  }, "message-bus: dropped envelope (missing channelId or messageId — cannot correlate reply)");
49
65
  return;
50
66
  }
51
- if (opts.register.isBusy()) {
67
+ // Layer 3 — physical-device-serial gate. The agent has ONE device
68
+ // and ONE in-flight task slot. Concurrent invocations get an
69
+ // `agent_busy` reply so callers can retry instead of hanging on
70
+ // /wait.
71
+ if (opts.sessionManager.isTurnInFlight()) {
52
72
  opts.logger.warn({
53
73
  incomingMessageId: prompt.messageId,
54
74
  incomingChannelId: prompt.channelId,
55
- activeTurn: opts.register.current_()?.taskId,
56
75
  }, "message-bus: agent busy — sending agent_busy reply");
57
76
  await opts.busyReply.replyBusy({
58
77
  channelId: prompt.channelId,
@@ -61,47 +80,91 @@ export function buildMessageBusInboundHandler(opts) {
61
80
  });
62
81
  return;
63
82
  }
64
- // Gap E: forward `prompt.files` URLs into the prompt text so the
65
- // agent at least sees them. beeos-claw's localServer.agentSend takes
66
- // files as a separate argument; device-agent's host.submitTask only
67
- // accepts a single string today. Append a minimal `[attachments]`
68
- // block — deeper resource-resolver integration is out of scope here.
69
- const promptText = appendAttachments(prompt.message, prompt.files);
70
- const submitted = opts.host.submitTask(promptText, {
71
- correlationTag: `ms:${prompt.channelId}`,
72
- });
73
- const claimed = opts.register.claim({
74
- taskId: submitted.taskId,
83
+ // Per-channel session anchor. `sessionKey` is the canonical id —
84
+ // multiple chat_message envelopes carrying the same `context_id`
85
+ // (or `channel_id` fallback) flow into the same sid, so the
86
+ // history-injection preamble in `_executePrompt` can stitch them
87
+ // into a continuous conversation.
88
+ const sid = prompt.sessionKey;
89
+ const notifier = new MsChannelNotifier({
90
+ sdk: opts.sdk,
75
91
  channelId: prompt.channelId,
76
92
  inboundMessageId: prompt.messageId,
77
93
  sessionKey: prompt.sessionKey,
78
- contextId: prompt.contextId,
94
+ logger: opts.logger,
79
95
  });
80
- if (!claimed) {
81
- // Race between two near-simultaneous publications — undo the
82
- // submit and treat the second one as busy. host.cancel returns
83
- // `false` if the task already finished, which is fine.
84
- opts.host.cancel(submitted.taskId);
85
- opts.logger.warn({ messageId: prompt.messageId }, "message-bus: turn-context race — cancelling and replying busy");
86
- await opts.busyReply.replyBusy({
87
- channelId: prompt.channelId,
88
- inboundMessageId: prompt.messageId,
89
- sessionKey: prompt.sessionKey,
90
- });
91
- return;
92
- }
96
+ opts.sessionManager.bindNotifier(sid, notifier);
97
+ opts.tracker.set({
98
+ messageId: prompt.messageId,
99
+ channelId: prompt.channelId,
100
+ sid,
101
+ });
102
+ const promptText = appendAttachments(prompt.message, prompt.files);
93
103
  opts.logger.info({
94
- taskId: submitted.taskId,
104
+ sid,
95
105
  channelId: prompt.channelId,
96
106
  inboundMessageId: prompt.messageId,
97
107
  contextId: prompt.contextId,
98
108
  sessionKey: prompt.sessionKey,
109
+ files: prompt.files.length,
99
110
  }, "message-bus: turn started");
100
- // Surface late submission failures into the outbound stream so the
101
- // caller's /wait does not hang. The outbound listener already
102
- // emits an agent_reply on `task:end (error)`; we do NOT await
103
- // `submitted.done` here because the listener handles all terminal
104
- // events.
111
+ try {
112
+ const result = await opts.sessionManager.promptForSession(sid, promptText);
113
+ opts.logger.info({
114
+ sid,
115
+ channelId: prompt.channelId,
116
+ inboundMessageId: prompt.messageId,
117
+ stopReason: result.stopReason,
118
+ }, "message-bus: turn finished");
119
+ }
120
+ catch (err) {
121
+ const message = err instanceof Error ? err.message : String(err);
122
+ opts.logger.warn({
123
+ sid,
124
+ channelId: prompt.channelId,
125
+ inboundMessageId: prompt.messageId,
126
+ err: message,
127
+ }, "message-bus: promptForSession rejected — emitting fallback reply");
128
+ // If the notifier already published the terminal `agent_reply`
129
+ // (normal stream_complete path) the second publish is harmless
130
+ // because the notifier guards on `finalized`.
131
+ if (message === "agent_busy") {
132
+ await opts.busyReply.replyBusy({
133
+ channelId: prompt.channelId,
134
+ inboundMessageId: prompt.messageId,
135
+ sessionKey: prompt.sessionKey,
136
+ });
137
+ }
138
+ else if (!notifier.isFinalized) {
139
+ // Surface the error as a terminal agent_reply so callers see
140
+ // a definite outcome instead of a /wait timeout. Use durable
141
+ // sendChannelMessage (same path as MsChannelNotifier.publishFinal)
142
+ // so caller's /wait can match via DB even if it's mid-reconnect.
143
+ try {
144
+ await opts.sdk.sendChannelMessage(prompt.channelId, {
145
+ type: "agent_reply",
146
+ payload: {
147
+ session_key: prompt.sessionKey,
148
+ error: message,
149
+ text: notifier.currentText,
150
+ },
151
+ inReplyTo: prompt.messageId,
152
+ });
153
+ }
154
+ catch (publishErr) {
155
+ opts.logger.error({
156
+ err: publishErr instanceof Error
157
+ ? publishErr.message
158
+ : String(publishErr),
159
+ channelId: prompt.channelId,
160
+ }, "message-bus: failed to publish fallback agent_reply");
161
+ }
162
+ }
163
+ }
164
+ finally {
165
+ opts.sessionManager.unbindNotifier(sid);
166
+ opts.tracker.clear(prompt.messageId);
167
+ }
105
168
  };
106
169
  }
107
170
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"inbound.js","sourceRoot":"","sources":["../../../src/plugins/message-bus/inbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACL,iBAAiB,GAElB,MAAM,uBAAuB,CAAC;AA2B/B;;;;;GAKG;AACH,MAAM,UAAU,6BAA6B,CAC3C,IAA8B;IAE9B,OAAO,KAAK,EAAE,QAAQ,EAAE,EAAE;QACxB,MAAM,MAAM,GAAG,iBAAiB,CAAC,QAAQ,EAAE;YACzC,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,EAAE;SACtC,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,EACxD,8DAA8D,CAC/D,CAAC;YACF,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YAC3C,IAAI,CAAC,MAAM,CAAC,IAAI,CACd;gBACE,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS;gBAChC,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS;aACjC,EACD,yFAAyF,CAC1F,CAAC;YACF,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,CACd;gBACE,iBAAiB,EAAE,MAAM,CAAC,SAAS;gBACnC,iBAAiB,EAAE,MAAM,CAAC,SAAS;gBACnC,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,MAAM;aAC7C,EACD,oDAAoD,CACrD,CAAC;YACF,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;gBAC7B,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,gBAAgB,EAAE,MAAM,CAAC,SAAS;gBAClC,UAAU,EAAE,MAAM,CAAC,UAAU;aAC9B,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,iEAAiE;QACjE,qEAAqE;QACrE,oEAAoE;QACpE,kEAAkE;QAClE,qEAAqE;QACrE,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAEnE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE;YACjD,cAAc,EAAE,MAAM,MAAM,CAAC,SAAS,EAAE;SACzC,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAClC,MAAM,EAAE,SAAS,CAAC,MAAM;YACxB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,gBAAgB,EAAE,MAAM,CAAC,SAAS;YAClC,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,6DAA6D;YAC7D,+DAA+D;YAC/D,uDAAuD;YACvD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YACnC,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,EAC/B,+DAA+D,CAChE,CAAC;YACF,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;gBAC7B,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,gBAAgB,EAAE,MAAM,CAAC,SAAS;gBAClC,UAAU,EAAE,MAAM,CAAC,UAAU;aAC9B,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,IAAI,CACd;YACE,MAAM,EAAE,SAAS,CAAC,MAAM;YACxB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,gBAAgB,EAAE,MAAM,CAAC,SAAS;YAClC,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,UAAU,EAAE,MAAM,CAAC,UAAU;SAC9B,EACD,2BAA2B,CAC5B,CAAC;QAEF,mEAAmE;QACnE,8DAA8D;QAC9D,8DAA8D;QAC9D,kEAAkE;QAClE,UAAU;IACZ,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,iBAAiB,CAAC,OAAe,EAAE,KAAwB;IAClE,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACjD,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnD,OAAO,GAAG,OAAO,sBAAsB,IAAI,EAAE,CAAC;AAChD,CAAC"}
1
+ {"version":3,"file":"inbound.js","sourceRoot":"","sources":["../../../src/plugins/message-bus/inbound.ts"],"names":[],"mappings":"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"}
@@ -94,8 +94,7 @@ export declare class MessageBusPlugin implements AgentPlugin {
94
94
  readonly name = "message-bus";
95
95
  private ctx?;
96
96
  private sdk;
97
- private register;
98
- private unsubscribeOutbound;
97
+ private tracker;
99
98
  private resolvedPrincipalId;
100
99
  private stopped;
101
100
  private retryHandle;
@@ -31,9 +31,9 @@
31
31
  import { MessageSDK } from "@beeos-ai/message-sdk";
32
32
  import { createDeviceMessagingTokenProvider } from "../../messaging/token-provider.js";
33
33
  import { MessageSdkBusyReplyHandler } from "./busy-reply.js";
34
+ import { buildMessageBusCancelHandler, MESSAGE_TYPE_CHAT_CANCEL, } from "./cancel.js";
34
35
  import { buildMessageBusInboundHandler } from "./inbound.js";
35
- import { installMessageBusOutbound } from "./outbound.js";
36
- import { TurnContextRegister } from "./turn-context.js";
36
+ import { MsInflightTracker } from "./ms-inflight-tracker.js";
37
37
  /**
38
38
  * Backoff schedule for token / connect failures (milliseconds). Each
39
39
  * entry is the delay BEFORE the next retry. The last entry caps the
@@ -57,8 +57,7 @@ export class MessageBusPlugin {
57
57
  name = "message-bus";
58
58
  ctx;
59
59
  sdk = null;
60
- register = null;
61
- unsubscribeOutbound = null;
60
+ tracker = null;
62
61
  resolvedPrincipalId = "";
63
62
  // Connect-loop state.
64
63
  stopped = false;
@@ -117,15 +116,6 @@ export class MessageBusPlugin {
117
116
  // Resolve any outstanding waiter so test code that awaits
118
117
  // `whenSettled()` does not hang past stop().
119
118
  this.resolveConnectSettled();
120
- if (this.unsubscribeOutbound) {
121
- try {
122
- this.unsubscribeOutbound();
123
- }
124
- catch {
125
- /* ignore */
126
- }
127
- this.unsubscribeOutbound = null;
128
- }
129
119
  if (this.sdk) {
130
120
  try {
131
121
  await this.sdk.disconnect();
@@ -135,9 +125,9 @@ export class MessageBusPlugin {
135
125
  }
136
126
  this.sdk = null;
137
127
  }
138
- if (this.register) {
139
- this.register.reset();
140
- this.register = null;
128
+ if (this.tracker) {
129
+ this.tracker.reset();
130
+ this.tracker = null;
141
131
  }
142
132
  }
143
133
  // Test seam — exposes the resolved principal so integration tests can
@@ -199,8 +189,12 @@ export class MessageBusPlugin {
199
189
  createDeviceMessagingTokenProvider({ agentGatewayUrl, keypair });
200
190
  const sdk = this.opts.sdkFactory?.(log) ??
201
191
  new MessageSDK({
202
- // No serviceUrl — agents talk to MS only via Agent Gateway
203
- // proxy + Centrifugo (P6 boundary).
192
+ // serviceUrl is discovered post-token (tok.service_url) and
193
+ // late-bound via setServiceUrl below. Once bound, the SDK
194
+ // forwards the Centrifugo token as Authorization: Bearer
195
+ // for principal-level REST auth — agent never sees the MS
196
+ // X-API-Key. This is the durable publish path used by
197
+ // sendChannelMessage for terminal agent_reply.
204
198
  centrifugoUrl: ctx.config.messageBus.centrifugoUrl || undefined,
205
199
  }, wrapLoggerForSdk(log));
206
200
  let tok;
@@ -216,20 +210,31 @@ export class MessageBusPlugin {
216
210
  this.logAttemptFailure(attempt, "gateway returned empty principal_id", null, log);
217
211
  return false;
218
212
  }
219
- const register = new TurnContextRegister();
213
+ // Late-bind MS REST URL discovered from the token response so
214
+ // sdk.sendChannelMessage (used by MsChannelNotifier.publishFinal
215
+ // for durable terminal replies) has somewhere to POST. When
216
+ // service_url is empty (legacy gateway not exposing it yet), the
217
+ // SDK keeps `serviceUrl: undefined` and any sendChannelMessage call
218
+ // throws — that's the explicit failure mode preferred over silently
219
+ // falling back to lossy Centrifugo direct publish.
220
+ sdk.setServiceUrl(tok.service_url);
221
+ const tracker = new MsInflightTracker();
220
222
  const busyReply = new MessageSdkBusyReplyHandler({ sdk });
223
+ const sessionManager = ctx.sessionManager;
221
224
  const chatHandler = buildMessageBusInboundHandler({
222
- host: ctx.host,
223
- register,
225
+ sessionManager,
226
+ sdk,
224
227
  busyReply,
228
+ tracker,
225
229
  logger: log,
226
230
  localAgentId: this.opts.localAgentId,
227
231
  source: "chat_message",
228
232
  });
229
233
  const legacyHandler = buildMessageBusInboundHandler({
230
- host: ctx.host,
231
- register,
234
+ sessionManager,
235
+ sdk,
232
236
  busyReply,
237
+ tracker,
233
238
  logger: log,
234
239
  localAgentId: this.opts.localAgentId,
235
240
  source: "agent_request",
@@ -245,12 +250,17 @@ export class MessageBusPlugin {
245
250
  }
246
251
  await legacyHandler(env);
247
252
  });
248
- const unsubscribe = installMessageBusOutbound({
249
- host: ctx.host,
250
- sdk,
251
- register,
253
+ // Cooperative cancel: the platform pushes `chat_cancel` envelopes
254
+ // when the caller hangs up / explicitly cancels / times out. We
255
+ // address the in-flight turn IM-natively by `target_message_id`
256
+ // (== inbound chat_message.message_id) so the agent stays free of
257
+ // A2A / MCP / OpenAPI protocol knowledge.
258
+ const cancelHandler = buildMessageBusCancelHandler({
259
+ sessionManager,
260
+ tracker,
252
261
  logger: log,
253
262
  });
263
+ sdk.onMessage(MESSAGE_TYPE_CHAT_CANCEL, async (env) => cancelHandler(env));
254
264
  sdk.onceConnected(() => {
255
265
  log.info({
256
266
  principalId,
@@ -266,14 +276,7 @@ export class MessageBusPlugin {
266
276
  });
267
277
  }
268
278
  catch (err) {
269
- // Roll back the listener so we don't double-subscribe on the
270
- // next retry, and disconnect the half-opened SDK.
271
- try {
272
- unsubscribe();
273
- }
274
- catch {
275
- /* ignore */
276
- }
279
+ // Disconnect the half-opened SDK so the next retry starts clean.
277
280
  try {
278
281
  await sdk.disconnect();
279
282
  }
@@ -284,8 +287,7 @@ export class MessageBusPlugin {
284
287
  return false;
285
288
  }
286
289
  this.sdk = sdk;
287
- this.register = register;
288
- this.unsubscribeOutbound = unsubscribe;
290
+ this.tracker = tracker;
289
291
  this.resolvedPrincipalId = principalId;
290
292
  return true;
291
293
  }