@ziggs-ai/agent-sdk 0.20.0 → 0.21.0

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 (51) hide show
  1. package/dist/Agent.d.ts +28 -7
  2. package/dist/Agent.js +176 -71
  3. package/dist/brain/ClaudeBrain.d.ts +11 -1
  4. package/dist/brain/ClaudeBrain.js +71 -18
  5. package/dist/brain/ZiggsBrain.d.ts +2 -0
  6. package/dist/brain/ZiggsBrain.js +1 -0
  7. package/dist/brain/claudeCognition.d.ts +1 -1
  8. package/dist/brain/claudeCognition.js +47 -7
  9. package/dist/brain/claudeSdkProtocol.d.ts +1 -1
  10. package/dist/brain/claudeSdkProtocol.js +1 -1
  11. package/dist/brain/claudeSdkTools.d.ts +3 -0
  12. package/dist/brain/claudeSdkTools.js +4 -2
  13. package/dist/brain/hireActivationNotice.d.ts +19 -0
  14. package/dist/brain/hireActivationNotice.js +79 -0
  15. package/dist/brain/standingOfferNotice.d.ts +15 -0
  16. package/dist/brain/standingOfferNotice.js +89 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/ingress/normalizeIncoming.d.ts +3 -0
  20. package/dist/ingress/normalizeIncoming.js +8 -0
  21. package/dist/mcp/createMcpAgent.d.ts +1 -1
  22. package/dist/mcp/createMcpAgent.js +1 -1
  23. package/dist/memory/MemoryStore.d.ts +18 -4
  24. package/dist/memory/MemoryStore.js +52 -12
  25. package/dist/platform/ConnectionPool.d.ts +9 -4
  26. package/dist/platform/ConnectionPool.js +16 -6
  27. package/dist/platform/StateTimers.d.ts +12 -6
  28. package/dist/platform/StateTimers.js +26 -6
  29. package/dist/platform/ZiggsEffectHandler.js +5 -1
  30. package/dist/platform/inboxLoop.d.ts +5 -7
  31. package/dist/platform/inboxLoop.js +36 -80
  32. package/dist/platform/polls/declaredPollRunner.d.ts +7 -1
  33. package/dist/platform/polls/declaredPollRunner.js +7 -9
  34. package/dist/platform/tools/index.d.ts +1 -0
  35. package/dist/platform/tools/index.js +1 -0
  36. package/dist/platform/tools/tier2/contextTools.d.ts +2 -0
  37. package/dist/platform/tools/tier2/contextTools.js +3 -1
  38. package/dist/platform/tools/tier2/planTools.d.ts +20 -0
  39. package/dist/platform/tools/tier2/planTools.js +64 -0
  40. package/dist/pricing/fleetDefaults.js +4 -9
  41. package/dist/pricing/fleetTierByAgentId.gen.d.ts +1 -1
  42. package/dist/pricing/fleetTierByAgentId.gen.js +2 -752
  43. package/dist/runtime/AgentMachine.d.ts +3 -0
  44. package/dist/runtime/AgentMachine.js +6 -1
  45. package/dist/runtime/runTurn.d.ts +1 -0
  46. package/dist/runtime/runTurn.js +19 -15
  47. package/dist/runtime/wakeEffects.d.ts +35 -0
  48. package/dist/runtime/wakeEffects.js +121 -0
  49. package/package.json +2 -2
  50. package/dist/pricing/fleetFreeTierA.gen.d.ts +0 -6
  51. package/dist/pricing/fleetFreeTierA.gen.js +0 -11
@@ -192,7 +192,7 @@ export declare function trimDescription(desc: string | undefined): string | unde
192
192
  * await agent.connectAsync();
193
193
  *
194
194
  * process.on('SIGTERM', async () => {
195
- * agent.disconnect();
195
+ * await agent.disconnect();
196
196
  * await agent.drain();
197
197
  * await closeMcp();
198
198
  * });
@@ -333,7 +333,7 @@ function capTools(tools, agentId, maxTools, extraToolCount, allowedTools) {
333
333
  * await agent.connectAsync();
334
334
  *
335
335
  * process.on('SIGTERM', async () => {
336
- * agent.disconnect();
336
+ * await agent.disconnect();
337
337
  * await agent.drain();
338
338
  * await closeMcp();
339
339
  * });
@@ -56,15 +56,29 @@ export declare class FileMemoryStore implements MemoryStore {
56
56
  delete(key: string): Promise<void>;
57
57
  }
58
58
  /**
59
- * Section a wake may read and write. A hire (pinned agreement, or an
60
- * `agrn-` lane) gets its own prefix. A wake with no hire shares the
61
- * agent's own section — same as before for in-process tools that key by
62
- * chat themselves.
59
+ * Section a wake may read and write.
60
+ *
61
+ * Pick from the hire this wake is acting under — the same fact the
62
+ * server uses for acting-for: a pinned agreement, the delivery / event
63
+ * hire, or an `agrn-` lane. A chat-room wake with exactly one hire
64
+ * behind it lands in that hire's section. Zero hires, or more than one
65
+ * customer present, share the agent's own section — the "operator only"
66
+ * answer in memory form. Never pick from the chat id alone.
63
67
  */
64
68
  export declare function memorySectionForWake(input: {
65
69
  sessionId?: string | null;
66
70
  activeAgreementId?: string | null;
71
+ /** Hire ids already on this wake (delivery / event / acting-for). */
72
+ wakeAgreementIds?: readonly (string | null | undefined)[];
67
73
  }): string;
74
+ /** Distinct non-empty hire ids, first-seen order. */
75
+ export declare function uniqueHireIds(raw: readonly (string | null | undefined)[]): string[];
76
+ /**
77
+ * Hire ids the waking event already named. A chat-room message that
78
+ * carries the delivery's `agreementId` is how two customer rooms
79
+ * partition without a pinned ctx.
80
+ */
81
+ export declare function hireIdsFromEvent(event: unknown): string[];
68
82
  /**
69
83
  * Present a store as if it only contained one section. Agent keys stay
70
84
  * unchanged; get/put/list/delete all go through `section:`.
@@ -78,22 +78,62 @@ export class FileMemoryStore {
78
78
  }
79
79
  }
80
80
  /**
81
- * Section a wake may read and write. A hire (pinned agreement, or an
82
- * `agrn-` lane) gets its own prefix. A wake with no hire shares the
83
- * agent's own section — same as before for in-process tools that key by
84
- * chat themselves.
81
+ * Section a wake may read and write.
82
+ *
83
+ * Pick from the hire this wake is acting under — the same fact the
84
+ * server uses for acting-for: a pinned agreement, the delivery / event
85
+ * hire, or an `agrn-` lane. A chat-room wake with exactly one hire
86
+ * behind it lands in that hire's section. Zero hires, or more than one
87
+ * customer present, share the agent's own section — the "operator only"
88
+ * answer in memory form. Never pick from the chat id alone.
85
89
  */
86
90
  export function memorySectionForWake(input) {
87
- const pinned = typeof input.activeAgreementId === 'string'
88
- ? input.activeAgreementId.trim()
89
- : '';
90
- if (pinned)
91
- return `hire:${pinned}`;
92
- const fromLane = agreementIdFromAgrnSession(input.sessionId);
93
- if (fromLane)
94
- return `hire:${fromLane}`;
91
+ const ids = uniqueHireIds([
92
+ input.activeAgreementId,
93
+ ...(input.wakeAgreementIds ?? []),
94
+ agreementIdFromAgrnSession(input.sessionId),
95
+ ]);
96
+ if (ids.length === 1)
97
+ return `hire:${ids[0]}`;
95
98
  return 'agent';
96
99
  }
100
+ /** Distinct non-empty hire ids, first-seen order. */
101
+ export function uniqueHireIds(raw) {
102
+ const out = [];
103
+ const seen = new Set();
104
+ for (const value of raw) {
105
+ const id = typeof value === 'string' ? value.trim() : '';
106
+ if (!id || seen.has(id))
107
+ continue;
108
+ seen.add(id);
109
+ out.push(id);
110
+ }
111
+ return out;
112
+ }
113
+ /**
114
+ * Hire ids the waking event already named. A chat-room message that
115
+ * carries the delivery's `agreementId` is how two customer rooms
116
+ * partition without a pinned ctx.
117
+ */
118
+ export function hireIdsFromEvent(event) {
119
+ if (!event || typeof event !== 'object')
120
+ return [];
121
+ const rec = event;
122
+ const resource = rec.resource && typeof rec.resource === 'object'
123
+ ? rec.resource
124
+ : {};
125
+ const result = rec.result && typeof rec.result === 'object'
126
+ ? rec.result
127
+ : {};
128
+ return uniqueHireIds([
129
+ typeof rec.agreementId === 'string' ? rec.agreementId : null,
130
+ typeof resource.agreementId === 'string' ? resource.agreementId : null,
131
+ typeof result.agreementId === 'string' ? result.agreementId : null,
132
+ rec.kind === 'agreement' && typeof rec.resourceId === 'string'
133
+ ? rec.resourceId
134
+ : null,
135
+ ]);
136
+ }
97
137
  function agreementIdFromAgrnSession(sessionId) {
98
138
  return agreementIdFromLane(sessionId)?.trim() || null;
99
139
  }
@@ -19,12 +19,17 @@ export declare class ConnectionPool {
19
19
  private readonly hosts;
20
20
  private readonly responseListeners;
21
21
  /**
22
- * Register N agent configs. Nothing connects until `connectAll()`; a config
23
- * registered and never connected is an agent nobody hosts, which is the
24
- * failure this pool exists to make impossible.
22
+ * Register N agent configs. Nothing connects until `connectAll()`. A config
23
+ * that cannot start is reported and left down; the others still host. Only
24
+ * a launch where nobody came up is refused.
25
25
  */
26
26
  register(configs: AgentOptions[], metaArr?: ConnectionManagerMeta[]): void;
27
- /** Connect every registered agent. Reports the ones that could not come up. */
27
+ /**
28
+ * Connect every registered agent. One host that cannot come up is reported
29
+ * and does not take the rest down — a fleet where 28 of 29 are live is not
30
+ * a launch worth aborting. Zero started is: the process has nothing to
31
+ * host and must restart.
32
+ */
28
33
  connectAll(): Promise<void>;
29
34
  /** The host for one agent, connecting it if it somehow is not up yet. */
30
35
  startAgent(agentId: string): Promise<Agent>;
@@ -26,9 +26,9 @@ export class ConnectionPool {
26
26
  // Listeners enqueued by requestFrom — drained on outbound send-message effects.
27
27
  responseListeners = new Map();
28
28
  /**
29
- * Register N agent configs. Nothing connects until `connectAll()`; a config
30
- * registered and never connected is an agent nobody hosts, which is the
31
- * failure this pool exists to make impossible.
29
+ * Register N agent configs. Nothing connects until `connectAll()`. A config
30
+ * that cannot start is reported and left down; the others still host. Only
31
+ * a launch where nobody came up is refused.
32
32
  */
33
33
  register(configs, metaArr = []) {
34
34
  const metaByAgentId = new Map();
@@ -64,18 +64,28 @@ export class ConnectionPool {
64
64
  runtimeLog.debug('ConnectionPool', `connected "${agentId}"`);
65
65
  return host;
66
66
  }, async (host) => {
67
- host.disconnect();
67
+ await host.disconnect();
68
68
  this.hosts.delete(agentId);
69
69
  runtimeLog.debug('ConnectionPool', `disconnected "${agentId}"`);
70
70
  }, metaByAgentId.get(agentId));
71
71
  }
72
72
  }
73
- /** Connect every registered agent. Reports the ones that could not come up. */
73
+ /**
74
+ * Connect every registered agent. One host that cannot come up is reported
75
+ * and does not take the rest down — a fleet where 28 of 29 are live is not
76
+ * a launch worth aborting. Zero started is: the process has nothing to
77
+ * host and must restart.
78
+ */
74
79
  async connectAll() {
75
80
  await this.manager.connectAll();
76
81
  const connected = this.listActive().length;
82
+ if (this.size === 0)
83
+ return;
84
+ if (connected === 0) {
85
+ throw new Error(`0/${this.size} agents started; process must restart`);
86
+ }
77
87
  if (connected !== this.size) {
78
- throw new Error(`Only ${connected}/${this.size} agents started; process must restart`);
88
+ runtimeLog.warn('ConnectionPool', `Only ${connected}/${this.size} agents started — hosting the ones that came up. One host must not take the rest down.`);
79
89
  }
80
90
  }
81
91
  /** The host for one agent, connecting it if it somehow is not up yet. */
@@ -1,3 +1,4 @@
1
+ import type { WorkScope } from '../runtime/wakeEffects.js';
1
2
  export interface StateTimeoutEvent {
2
3
  type: 'state-timeout';
3
4
  state: string;
@@ -13,7 +14,7 @@ export interface StateTimeoutEvent {
13
14
  * check-back is none of that: it belongs to the agent's own plan, not to a
14
15
  * state's declared timeout, and the Claude brain has no states at all.
15
16
  */
16
- export interface WakeAtEvent {
17
+ export interface WakeAtEvent extends WorkScope {
17
18
  type: 'wake-at';
18
19
  /** What the agent said it was waiting for. Free text, for the brain to read. */
19
20
  reason: string | null;
@@ -35,7 +36,7 @@ export declare const MAX_WAKE_DELAY_MS: number;
35
36
  * (caller-provided enqueue → EventQueue), so it serializes behind any real
36
37
  * event and can never race one. Epochs make stale frames identifiable: a
37
38
  * queued timeout frame whose epoch is not the lane's currently-armed epoch was
38
- * superseded (a tick ran after it fired) and must be dropped.
39
+ * cancelled or replaced and must be dropped.
39
40
  *
40
41
  * In-memory only: process restart loses all timers; each lane re-arms on its
41
42
  * next real event (sessionStore persists { ctx, state } but not timers).
@@ -66,13 +67,12 @@ export declare class StateTimers {
66
67
  * the agent changing its mind, which is allowed and is why this replaces
67
68
  * rather than adds.
68
69
  *
69
- * In memory only. A restart loses it and the lane re-arms on its next real
70
- * event, so a lost check-back is late rather than gone — persistence is
71
- * listed Later on the ticket. Returns what actually got armed so the caller
70
+ * In memory only. A restart loses it; recovery requires the agent to re-arm
71
+ * on its next real event. Returns what actually got armed so the caller
72
72
  * can tell the agent, since "I will wake at X" is a promise the agent will
73
73
  * otherwise make on its own.
74
74
  */
75
- armWakeAt(laneKey: string, at: Date | number, reason?: string | null): {
75
+ armWakeAt(laneKey: string, at: Date | number, reason?: string | null, scope?: WorkScope): {
76
76
  firesAt: number;
77
77
  delayMs: number;
78
78
  };
@@ -82,6 +82,12 @@ export declare class StateTimers {
82
82
  firesAt: number;
83
83
  reason: string | null;
84
84
  } | null;
85
+ /**
86
+ * Consume the timer for a frame that survived stripStale. Liveness tracks
87
+ * inactivity, so any event resets it. An explicit wake is an appointment:
88
+ * unrelated traffic must leave it armed, even after it fired and queued.
89
+ */
90
+ consumeForEvent(event: unknown, laneKey: string): void;
85
91
  clear(laneKey: string): void;
86
92
  clearAll(): void;
87
93
  /**
@@ -12,7 +12,7 @@ export const MAX_WAKE_DELAY_MS = 2 ** 31 - 1;
12
12
  * (caller-provided enqueue → EventQueue), so it serializes behind any real
13
13
  * event and can never race one. Epochs make stale frames identifiable: a
14
14
  * queued timeout frame whose epoch is not the lane's currently-armed epoch was
15
- * superseded (a tick ran after it fired) and must be dropped.
15
+ * cancelled or replaced and must be dropped.
16
16
  *
17
17
  * In-memory only: process restart loses all timers; each lane re-arms on its
18
18
  * next real event (sessionStore persists { ctx, state } but not timers).
@@ -68,17 +68,17 @@ export class StateTimers {
68
68
  * the agent changing its mind, which is allowed and is why this replaces
69
69
  * rather than adds.
70
70
  *
71
- * In memory only. A restart loses it and the lane re-arms on its next real
72
- * event, so a lost check-back is late rather than gone — persistence is
73
- * listed Later on the ticket. Returns what actually got armed so the caller
71
+ * In memory only. A restart loses it; recovery requires the agent to re-arm
72
+ * on its next real event. Returns what actually got armed so the caller
74
73
  * can tell the agent, since "I will wake at X" is a promise the agent will
75
74
  * otherwise make on its own.
76
75
  */
77
- armWakeAt(laneKey, at, reason = null) {
76
+ armWakeAt(laneKey, at, reason = null, scope) {
78
77
  const firesAtRaw = at instanceof Date ? at.getTime() : Number(at);
79
78
  if (!Number.isFinite(firesAtRaw)) {
80
79
  throw new Error('StateTimers.armWakeAt: `at` must be a Date or epoch ms');
81
80
  }
81
+ const wakeScope = { ...scope };
82
82
  const now = Date.now();
83
83
  // A wake already due fires on the next turn of the event loop rather than
84
84
  // being refused: "wake me at 09:00" arriving at 09:00:01 means now, and an
@@ -92,7 +92,7 @@ export class StateTimers {
92
92
  const epoch = ++this._epoch;
93
93
  const handle = setTimeout(() => {
94
94
  runtimeLog.debug('StateTimers', `wake-at fired laneKey=${laneKey} reason=${reason ?? 'none'} epoch=${epoch}`);
95
- this._enqueue({ type: 'wake-at', reason, scheduledFor: firesAtRaw, epoch }, laneKey);
95
+ this._enqueue({ type: 'wake-at', reason, scheduledFor: firesAtRaw, epoch, ...wakeScope }, laneKey);
96
96
  }, delayMs);
97
97
  handle.unref?.();
98
98
  this._timers.set(laneKey, {
@@ -112,6 +112,26 @@ export class StateTimers {
112
112
  const t = this._timers.get(laneKey);
113
113
  return t ? { kind: t.kind, firesAt: t.firesAt, reason: t.reason } : null;
114
114
  }
115
+ /**
116
+ * Consume the timer for a frame that survived stripStale. Liveness tracks
117
+ * inactivity, so any event resets it. An explicit wake is an appointment:
118
+ * unrelated traffic must leave it armed, even after it fired and queued.
119
+ */
120
+ consumeForEvent(event, laneKey) {
121
+ const pending = this._timers.get(laneKey);
122
+ if (!pending)
123
+ return;
124
+ const frame = event;
125
+ const events = frame?.type === 'batch' && Array.isArray(frame.events)
126
+ ? frame.events
127
+ : [event];
128
+ const consumesWake = events.some((entry) => {
129
+ const e = entry;
130
+ return e?.type === 'wake-at' && e.epoch === pending.epoch;
131
+ });
132
+ if (pending.kind === 'liveness' || consumesWake)
133
+ this.clear(laneKey);
134
+ }
115
135
  clear(laneKey) {
116
136
  const t = this._timers.get(laneKey);
117
137
  if (!t)
@@ -35,7 +35,11 @@ async function buildSnapshot(deps, chatId, agentId, opts = {}) {
35
35
  catch (err) {
36
36
  // Defensive: server normally signals a dead chat as 200 + chatMissing, but
37
37
  // a raw 404 degrades the same way so message wakes still skip it.
38
- if (err?.status === 404) {
38
+ const status = err?.status;
39
+ // 403 = exists but this agent has no grant. Same as deleted:
40
+ // no world to act in. Leaving it as an empty snapshot used to run a
41
+ // full turn against refusals.
42
+ if (status === 404 || status === 403) {
39
43
  return emptySnapshot(agentId, true);
40
44
  }
41
45
  runtimeLog.warn('ZiggsEffectHandler', `snapshot fetch failed: ${err?.message ?? err}`);
@@ -1,4 +1,4 @@
1
- import { type InboxChatNews, type InboxRequestRef, type InboxTaskRef, type Creds } from '@ziggs-ai/api-client';
1
+ import { type InboxChatNews, type InboxDeliveryRef, type InboxRequestRef, type InboxTaskRef, type Creds } from '@ziggs-ai/api-client';
2
2
  import { SeenMessages } from './SeenMessages.js';
3
3
  /**
4
4
  * Per-agent inbox long-poll loop (delivery law, phase 4). This is the ONLY way
@@ -60,12 +60,11 @@ export interface InboxPassResult {
60
60
  */
61
61
  export declare function chatsInWakeOrder(chats: InboxChatNews[]): InboxChatNews[];
62
62
  /**
63
- * Store publish waits on a chat reply, not inbox ack. The ordinary pass can
64
- * already be inside leftover mail when that room appears, so a side read
65
- * answers `probe_*` chats without waiting for the current pass to finish.
66
- * It never acks — the main pass still owns the watermark.
63
+ * The one hire a chat's assigned deliveries name, or null when the room
64
+ * has none or more than one. Memory (and acting-for) treat "several
65
+ * customers in one room" as the shared section.
67
66
  */
68
- export declare function answerPublishProbes(deps: InboxLoopDeps): Promise<number>;
67
+ export declare function soleHireAgreementIdForChat(deliveries: readonly Pick<InboxDeliveryRef, 'chatId' | 'agreementId' | 'assigneeId'>[], chatId: string, ownAgentId: string): string | null;
69
68
  /**
70
69
  * One long-poll pass: hold up to `waitSeconds` for new inbox content, then read
71
70
  * the deltas, deliver them into the host, and ack. Returns what it delivered.
@@ -114,5 +113,4 @@ export declare function heldPassAfter(prev: number, pass: Pick<InboxPassResult,
114
113
  */
115
114
  export declare function runInboxLoop(deps: InboxLoopDeps, opts?: {
116
115
  waitSeconds?: number;
117
- probePollMs?: number;
118
116
  }): InboxLoopHandle;
@@ -5,6 +5,17 @@ const READ_PAGE = 100;
5
5
  function log(label, msg) {
6
6
  runtimeLog.info(label, msg);
7
7
  }
8
+ function inboxClient(deps) {
9
+ return new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl, deps.creds.instanceId);
10
+ }
11
+ async function releaseInboxHostBestEffort(deps) {
12
+ try {
13
+ await inboxClient(deps).releaseHost();
14
+ }
15
+ catch (err) {
16
+ warn(deps.label ?? 'inboxLoop', `inbox host release failed: ${err?.message ?? err}`);
17
+ }
18
+ }
8
19
  function warn(label, msg) {
9
20
  runtimeLog.warn(label, msg);
10
21
  }
@@ -25,45 +36,6 @@ export function chatsInWakeOrder(chats) {
25
36
  function isPublishProbeChat(chatId) {
26
37
  return chatId.startsWith('probe_');
27
38
  }
28
- /**
29
- * Store publish waits on a chat reply, not inbox ack. The ordinary pass can
30
- * already be inside leftover mail when that room appears, so a side read
31
- * answers `probe_*` chats without waiting for the current pass to finish.
32
- * It never acks — the main pass still owns the watermark.
33
- */
34
- export async function answerPublishProbes(deps) {
35
- const inbox = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
36
- const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
37
- const envelope = await inbox.getInbox({});
38
- const probes = (envelope.chats ?? []).filter((c) => isPublishProbeChat(c.chatId));
39
- if (probes.length === 0)
40
- return 0;
41
- const mine = (d) => d.assigneeId === deps.ownAgentId;
42
- const earliestByChat = new Map();
43
- for (const d of envelope.deliveries) {
44
- if (!d.chatId || !mine(d) || !isPublishProbeChat(d.chatId))
45
- continue;
46
- const seen = earliestByChat.get(d.chatId);
47
- if (!seen || d.ts < seen)
48
- earliestByChat.set(d.chatId, d.ts);
49
- }
50
- const attempt = async (what, handover) => {
51
- try {
52
- await handover();
53
- return true;
54
- }
55
- catch (err) {
56
- warn(deps.label ?? 'inboxLoop', `publish-probe ${what} was not handled: ${err?.message ?? err}`);
57
- return false;
58
- }
59
- };
60
- let delivered = 0;
61
- for (const news of probes) {
62
- const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
63
- delivered += await deliverMessagesForChat(deps, news, earliest, reader, attempt);
64
- }
65
- return delivered;
66
- }
67
39
  /**
68
40
  * Standing proposals share the merged inbox visibility model with deliveries,
69
41
  * but older envelopes predate an explicit assignee stamp. Prefer the server's
@@ -78,6 +50,22 @@ function proposalIsMine(proposal, ownAgentId) {
78
50
  return (proposal.pendingApprovalPartyIds?.includes(ownAgentId) === true ||
79
51
  proposal.proposedTo === ownAgentId);
80
52
  }
53
+ /**
54
+ * The one hire a chat's assigned deliveries name, or null when the room
55
+ * has none or more than one. Memory (and acting-for) treat "several
56
+ * customers in one room" as the shared section.
57
+ */
58
+ export function soleHireAgreementIdForChat(deliveries, chatId, ownAgentId) {
59
+ const ids = new Set();
60
+ for (const d of deliveries) {
61
+ if (d.chatId !== chatId || !d.agreementId)
62
+ continue;
63
+ if (d.assigneeId && d.assigneeId !== ownAgentId)
64
+ continue;
65
+ ids.add(d.agreementId);
66
+ }
67
+ return ids.size === 1 ? [...ids][0] : null;
68
+ }
81
69
  /**
82
70
  * Read the messages behind one chat's deliveries and hand them to the host.
83
71
  *
@@ -85,7 +73,7 @@ function proposalIsMine(proposal, ownAgentId) {
85
73
  * reads through `/context/read`, where the agent's own grants fence what it is
86
74
  * allowed to see. A delivery reference is an address, never an authorisation.
87
75
  */
88
- async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt) {
76
+ async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt, hireAgreementId) {
89
77
  let delivered = 0;
90
78
  let remaining = news.count;
91
79
  // `after` is exclusive, so step back a millisecond to include the oldest
@@ -135,6 +123,7 @@ async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt) {
135
123
  // leaving it made ClaudeBrain treat the wake as notice-only.
136
124
  receiverId: deps.ownAgentId,
137
125
  receiver: { id: deps.ownAgentId },
126
+ ...(hireAgreementId ? { agreementId: hireAgreementId } : {}),
138
127
  }));
139
128
  if (handled)
140
129
  delivered++;
@@ -155,7 +144,7 @@ async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt) {
155
144
  */
156
145
  export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
157
146
  const label = deps.label ?? 'inboxLoop';
158
- const inbox = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
147
+ const inbox = inboxClient(deps);
159
148
  const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
160
149
  const envelope = await inbox.getInbox(waitSeconds > 0 ? { waitSeconds } : {});
161
150
  let messagesDelivered = 0;
@@ -244,7 +233,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
244
233
  for (const news of chatsInWakeOrder(envelope.chats ?? [])) {
245
234
  const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
246
235
  const failuresBefore = failures;
247
- const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt);
236
+ const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt, soleHireAgreementIdForChat(envelope.deliveries, news.chatId, deps.ownAgentId));
248
237
  messagesDelivered += n;
249
238
  if (n > 0)
250
239
  log(label, `chat ${news.chatId} delivered ${n} message(s)`);
@@ -409,16 +398,6 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
409
398
  * some defaults ship with. Lower it if your deployment sits behind a shorter one.
410
399
  */
411
400
  const DEFAULT_WAIT_SECONDS = 110;
412
- /**
413
- * Side-poll interval for store-publish `probe_*` rooms.
414
- *
415
- * Default **off**. A second GET /inbox beside the main long-poll burned the
416
- * shared poll ceiling: every hosted agent side-polled, probes got 429s, and
417
- * Register saw ALB HTML 504s. Main pass + probe-first ordering + pinned
418
- * `probe_*` deliveries are enough; set `probePollMs` only when you knowingly
419
- * need the extra loop.
420
- */
421
- const DEFAULT_PROBE_POLL_MS = 0;
422
401
  const ERROR_BACKOFF_BASE_MS = 1_000;
423
402
  const ERROR_BACKOFF_MAX_MS = 30_000;
424
403
  /**
@@ -455,7 +434,6 @@ function sleep(ms) {
455
434
  export function runInboxLoop(deps, opts = {}) {
456
435
  const label = deps.label ?? 'inboxLoop';
457
436
  const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
458
- const probePollMs = opts.probePollMs ?? DEFAULT_PROBE_POLL_MS;
459
437
  let running = true;
460
438
  let consecutiveErrors = 0;
461
439
  let heldPasses = 0;
@@ -501,32 +479,7 @@ export function runInboxLoop(deps, opts = {}) {
501
479
  }
502
480
  }
503
481
  })();
504
- const probes = (async () => {
505
- if (probePollMs <= 0)
506
- return;
507
- while (running) {
508
- const slice = 50;
509
- let left = probePollMs;
510
- while (left > 0 && running) {
511
- const step = Math.min(slice, left);
512
- await sleep(step);
513
- left -= step;
514
- }
515
- if (!running)
516
- break;
517
- try {
518
- const n = await answerPublishProbes(deps);
519
- if (n > 0)
520
- log(label, `publish-probe side poll delivered ${n} message(s)`);
521
- }
522
- catch (err) {
523
- if (!running)
524
- break;
525
- warn(label, `publish-probe side poll failed: ${err?.message ?? err}`);
526
- }
527
- }
528
- })();
529
- const done = Promise.all([main, probes]);
482
+ const done = main;
530
483
  return {
531
484
  get running() {
532
485
  return running;
@@ -539,7 +492,10 @@ export function runInboxLoop(deps, opts = {}) {
539
492
  },
540
493
  async stop() {
541
494
  running = false;
542
- await done.catch(() => { });
495
+ // Drop the lease immediately. Do not wait out a held poll — SIGTERM
496
+ // then process.exit cannot sit on the 110s wait.
497
+ await releaseInboxHostBestEffort(deps);
498
+ void done.catch(() => { });
543
499
  },
544
500
  };
545
501
  }
@@ -1,4 +1,10 @@
1
1
  import type { AgentOptions } from '../../Agent.js';
2
2
  import { PollRunner, type WakeTarget } from './PollRunner.js';
3
- /** Validate before starting any work, so a declared wake source cannot vanish. */
3
+ /**
4
+ * Build the runner that executes this agent's declared wakes.
5
+ *
6
+ * A malformed `polls` value is a programming error and still throws. A
7
+ * binding that names an unknown kind or has no chatId is not: PollRunner
8
+ * warns and skips it, so one agent's invalid poll cannot refuse the host.
9
+ */
4
10
  export declare function createDeclaredPollRunner(options: AgentOptions, target: WakeTarget): PollRunner | null;
@@ -1,6 +1,12 @@
1
1
  import { PollRegistry } from './PollKind.js';
2
2
  import { PollRunner } from './PollRunner.js';
3
- /** Validate before starting any work, so a declared wake source cannot vanish. */
3
+ /**
4
+ * Build the runner that executes this agent's declared wakes.
5
+ *
6
+ * A malformed `polls` value is a programming error and still throws. A
7
+ * binding that names an unknown kind or has no chatId is not: PollRunner
8
+ * warns and skips it, so one agent's invalid poll cannot refuse the host.
9
+ */
4
10
  export function createDeclaredPollRunner(options, target) {
5
11
  const registry = new PollRegistry();
6
12
  for (const kind of options.pollKinds ?? [])
@@ -9,14 +15,6 @@ export function createDeclaredPollRunner(options, target) {
9
15
  return null;
10
16
  if (!Array.isArray(options.polls))
11
17
  throw new Error('Agent: polls must be an array');
12
- for (const binding of options.polls) {
13
- if (!binding || !registry.get(binding.kind)) {
14
- throw new Error(`Agent ${options.agentId}: unknown poll kind "${binding?.kind}"; declare its implementation in pollKinds`);
15
- }
16
- if (typeof binding.chatId !== 'string' || !binding.chatId.trim()) {
17
- throw new Error(`Agent ${options.agentId}: poll "${binding.kind}" requires a chatId`);
18
- }
19
- }
20
18
  if (!options.polls.length)
21
19
  return null;
22
20
  return new PollRunner({
@@ -24,6 +24,7 @@ export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, reques
24
24
  export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
25
25
  export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
26
26
  export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
27
+ export { PLAN_TOOLS, taskReplacePlanTool } from './tier2/planTools.js';
27
28
  export { LINK_TOOLS, linkProposeTool, linkListTool, } from './tier2/linkTools.js';
28
29
  export { INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, } from './tier2/introductionTools.js';
29
30
  export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
@@ -28,6 +28,7 @@ export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReach
28
28
  // payment_list_grants). Always-available on the hosted agent — see CLAUDE_SDK_TOOLS.
29
29
  export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
30
30
  export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
31
+ export { PLAN_TOOLS, taskReplacePlanTool } from './tier2/planTools.js';
31
32
  // links (agreements with engagementKind 'link') — cross-org reach.
32
33
  ///1022: the rail is two tools; propose/claim/revoke ride the
33
34
  // agreement grammar.
@@ -1,5 +1,7 @@
1
1
  import type { ToolDefinition } from '../../../tools/defineTool.js';
2
2
  export declare const contextReadTool: ToolDefinition;
3
+ export declare const openTool: ToolDefinition;
4
+ export declare const accessExplainTool: ToolDefinition;
3
5
  export declare const contextDelegateTool: ToolDefinition;
4
6
  export declare const contextExpandReachTool: ToolDefinition;
5
7
  export declare const contextDiscoverGrantableTool: ToolDefinition;
@@ -1,9 +1,11 @@
1
- import { CONTEXT_CAPABILITIES, contextReadCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, } from '@ziggs-ai/api-client';
1
+ import { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
3
  // context grant tools ride the same api-client
4
4
  // clients as ziggs-mcp; the whole definition (schema + handler) is now shared
5
5
  // via the capability layer, so the two surfaces cannot fork.
6
6
  export const contextReadTool = toolFromCapability(contextReadCapability);
7
+ export const openTool = toolFromCapability(openCapability);
8
+ export const accessExplainTool = toolFromCapability(accessExplainCapability);
7
9
  export const contextDelegateTool = toolFromCapability(contextDelegateCapability);
8
10
  export const contextExpandReachTool = toolFromCapability(contextExpandReachCapability);
9
11
  export const contextDiscoverGrantableTool = toolFromCapability(contextDiscoverGrantableCapability);