@wowyuarm/dsh-agent-team 0.1.9 → 0.1.10

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 (54) hide show
  1. package/README.md +4 -11
  2. package/README.zh.md +4 -11
  3. package/package.json +53 -44
  4. package/packages/agent-team/README.md +2 -2
  5. package/packages/agent-team/README.zh.md +2 -1
  6. package/packages/agent-team/lib/context-management.js +8 -5
  7. package/packages/agent-team/lib/context-projection.js +12 -8
  8. package/packages/agent-team/lib/context-source.js +163 -23
  9. package/packages/agent-team/lib/index.js +174 -124
  10. package/packages/agent-team/lib/ledger.js +111 -30
  11. package/packages/agent-team/lib/member-time-context.js +126 -0
  12. package/packages/agent-team/lib/pressure-policy.js +1 -2
  13. package/packages/agent-team/lib/progress-nudge.js +12 -4
  14. package/packages/agent-team/lib/session-remediation.js +481 -0
  15. package/packages/agent-team/lib/spec.js +3 -2
  16. package/packages/agent-team/lib/time-format.js +56 -0
  17. package/packages/agent-team/lib/typert.host.js +82 -49
  18. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  19. package/packages/agent-team/lib/typert.remote-client.js +46 -25
  20. package/packages/agent-team/lib/types/context-management.d.ts +6 -1
  21. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/context-source.d.ts +70 -36
  24. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/index.d.ts +32 -7
  26. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/ledger.d.ts +25 -0
  28. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
  30. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
  31. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/session-remediation.d.ts +149 -0
  34. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
  35. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/time-format.d.ts +28 -0
  37. package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/types/entities.d.ts +8 -0
  39. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/types/requests-results.d.ts +6 -0
  41. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  42. package/packages/agent-team/preset/team-member/agent.cordis.yml +19 -4
  43. package/packages/client-agent-team/lib/client.js +80 -54
  44. package/packages/client-agent-team/lib/client.js.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamMessage.js +2 -2
  46. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +4 -4
  47. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  48. package/packages/client-agent-team/lib/types/client/index.js +10 -5
  49. package/packages/client-agent-team/lib/types/client/slots.d.ts +1 -1
  50. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  51. package/packages/tool-agent-team/README.md +8 -6
  52. package/packages/tool-agent-team/README.zh.md +8 -6
  53. package/packages/tool-agent-team/lib/index.js +302 -110
  54. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -1,11 +1,18 @@
1
1
  import { AgentTeamDmDeliveryError, markAgentTeamPreset } from '@wowyuarm/dsh-agent-team/host';
2
+ import { formatTeamTimestamp } from '@wowyuarm/dsh-agent-team/time-format';
2
3
  import { registerContextTools } from "./context-tools.js";
3
4
  import { defineTool } from '@deepseek-ai/dsh-tools';
4
5
  export const name = 'wowyuarm-agent-team-tools';
5
6
  export const inject = ['tools'];
6
- function activityFactView(sequence, activity, markers) {
7
+ /** One shared bound for every bounded anchor subject a render shows. */
8
+ const SUBJECT_BOUND = 80;
9
+ function claimView(claim) {
10
+ return { claimRef: claim.claimRef, direction: claim.direction, state: claim.state, owner: claim.owner };
11
+ }
12
+ function activityFactView(sequence, activity, markers, occurredAt) {
7
13
  return {
8
14
  sequence, kind: 'activity', activity: activity.kind, actor: activity.actor, taskRef: activity.taskRef,
15
+ ...(occurredAt === undefined ? {} : { occurredAt }),
9
16
  ...(activity.claimRef === undefined ? {} : { claimRef: activity.claimRef }),
10
17
  ...(activity.claimRefs === undefined || activity.claimRefs.length === 0 ? {} : { claimRefs: [...activity.claimRefs] }),
11
18
  ...(activity.completedClaimRefs === undefined || activity.completedClaimRefs.length === 0 ? {} : { completedClaimRefs: [...activity.completedClaimRefs] }),
@@ -23,6 +30,26 @@ function adviceView(advice) {
23
30
  action: advice.action, guidance: advice.guidance,
24
31
  };
25
32
  }
33
+ /**
34
+ * The one deterministic bounded subject, shared by directory rows and Thread
35
+ * orientation: whitespace collapses to one line, then one bounded cut with an
36
+ * explicit truncation mark. Directory, read, and history orientation must
37
+ * never disagree about what a Thread is about.
38
+ */
39
+ function boundedSubject(body) {
40
+ const collapsed = body.replaceAll(/\s+/gu, ' ').trim();
41
+ return collapsed.length <= SUBJECT_BOUND ? collapsed : `${collapsed.slice(0, SUBJECT_BOUND - 1)}…`;
42
+ }
43
+ /** Unread/direct markers sit on the fact line itself; a fact carrying neither renders neither. */
44
+ function factMarkers(fact) {
45
+ if (fact.unread === undefined)
46
+ return '';
47
+ if (fact.direct === true)
48
+ return ' [direct]';
49
+ if (fact.unread === true)
50
+ return ' [unread]';
51
+ return '';
52
+ }
26
53
  /** Render one structured activity fact as a self-describing decision-surface line. */
27
54
  function activityLine(fact) {
28
55
  const segments = [`${fact.sequence}`, fact.actor, fact.activity];
@@ -39,6 +66,16 @@ function activityLine(fact) {
39
66
  segments.push(`released claims ${fact.releasedClaimRefs.join(', ')}`);
40
67
  return segments.join(' ');
41
68
  }
69
+ /** The one hand-off line a fully drained read or a committed public mutation may render. */
70
+ function nextWriteLine(revision) {
71
+ return `Next write — baseRevision: ${revision} (copy exactly; never derive or cite).`;
72
+ }
73
+ /** Task standing inline on a Thread identity line; absent on taskless Threads. */
74
+ function taskStanding(value) {
75
+ if (value.taskRef === undefined)
76
+ return '';
77
+ return `${value.taskRef}${value.taskNumber === undefined ? '' : ` (#${value.taskNumber})`}${value.status === undefined ? '' : `, ${value.status}${value.resolution === undefined ? '' : `/${value.resolution}`}`}`;
78
+ }
42
79
  function service(agent) {
43
80
  const host = agent.ctx.get('agentTeam');
44
81
  if (host === undefined)
@@ -54,9 +91,41 @@ function member(agent) {
54
91
  function requestId(agentId, callId) {
55
92
  return `agent-team:tool:${agentId}:${callId}`;
56
93
  }
94
+ /**
95
+ * One typed-rejection renderer parameterized by the mutation noun, shared by
96
+ * team_message and team_claim. Outcome first, action-local recovery facts,
97
+ * then the single recovery route: read the named Thread and reconsider — a
98
+ * rejection carries no changed facts, so it never renders a numeric revision
99
+ * or a next-write token.
100
+ */
101
+ function rejectionLines(noun, value) {
102
+ const identity = [
103
+ value.threadRef === undefined ? '' : value.threadRef,
104
+ value.taskRef === undefined ? '' : value.taskRef,
105
+ ].filter(part => part !== '').join(' · ');
106
+ if (value.kind === 'unread_required') {
107
+ return [
108
+ 'Not committed — unread_required.',
109
+ `${identity}${identity === '' ? '' : ' · '}${value.unreadCount ?? 0} unread, ${value.directCount ?? 0} direct`,
110
+ `Read the pending updates with team_thread read before reconsidering this ${noun}.`,
111
+ ];
112
+ }
113
+ if (value.kind === 'stale_revision') {
114
+ return [
115
+ 'Not committed — the Thread changed after your last read (stale_revision).',
116
+ identity,
117
+ `Read the Thread, reconsider the new facts, then retry this ${noun} with the token that read returns.`,
118
+ ];
119
+ }
120
+ return [
121
+ 'Not committed — member_not_following.',
122
+ `${(value.memberIds ?? []).join(', ')} not following${value.threadRef === undefined ? '' : ` ${value.threadRef}`}${value.taskRef === undefined ? '' : ` (${value.taskRef})`}; no message was added.`,
123
+ 'Only a Human can invite an unfollowed Agent — retry without mentioning them, or ask the Human.',
124
+ ];
125
+ }
57
126
  const teamInbox = defineTool({
58
127
  name: 'team_inbox',
59
- description: 'List your bounded Team Inbox. It returns Thread summaries without message bodies and does not mark anything read.',
128
+ description: 'List your bounded Team Inbox for triage: unread Thread summaries with counts, without message bodies and without marking anything read. Read a selected Thread with team_thread read; the inbox itself authorizes no mutation.',
60
129
  parameters: { limit: { type: 'number' } },
61
130
  output: {
62
131
  schema: { type: 'object', additionalProperties: false, properties: {
@@ -64,12 +133,27 @@ const teamInbox = defineTool({
64
133
  items: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
65
134
  threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true },
66
135
  taskRef: { type: 'string' }, status: { type: 'string' }, revision: { type: 'number', required: true }, unreadCount: { type: 'number', required: true }, directCount: { type: 'number', required: true },
67
- taskNumber: { type: 'number' },
136
+ taskNumber: { type: 'number' }, newestOccurredAt: { type: 'string' },
68
137
  } } },
69
138
  } },
70
- render: (_args, value) => [{ type: 'text', text: value.items.length === 0 ? `No unread Team work.${value.totalUnreadCount > 0 ? ` (${value.totalUnreadCount} unread on Threads beyond this bounded list — call again with a larger limit.)` : ''}`
71
- : [`${value.totalUnreadCount} unread update(s) total, ${value.totalDirectCount} direct, across ${value.items.length} Thread(s) shown${value.totalUnreadCount > value.items.reduce((sum, item) => sum + item.unreadCount, 0) ? ' — more exist beyond this bounded list' : ''}`,
72
- ...value.items.map(item => `${item.threadRef}${item.channelRef === undefined ? '' : ` · ${item.channelRef}`}${item.taskRef === undefined ? '' : ` · ${item.taskRef}`}${item.taskNumber === undefined ? '' : ` (#${item.taskNumber})`}${item.status === undefined ? '' : ` (${item.status})`} · ${item.unreadCount} unread, ${item.directCount} direct, revision ${item.revision}`)].join('\n') }],
139
+ // Triage surface only: totals plus both counters per row (direct renders
140
+ // even when zero), a bounded-list conclusion so truncation can never read
141
+ // as drained, and one route to the tool that can actually acknowledge the
142
+ // work. No subject, no body, no revision label, no write token: the inbox
143
+ // routes to a required current read, which supplies the write basis.
144
+ render: (_args, value) => {
145
+ if (value.items.length === 0) {
146
+ return [{ type: 'text', text: value.totalUnreadCount > 0
147
+ ? `Inbox — ${value.totalUnreadCount} unread update(s) on Threads beyond this bounded list — call again with a larger limit.`
148
+ : 'Inbox empty — no unread Team work.' }];
149
+ }
150
+ const shown = value.items.reduce((sum, item) => sum + item.unreadCount, 0);
151
+ return [{ type: 'text', text: [
152
+ `Inbox — ${value.totalUnreadCount} unread update(s) total, ${value.totalDirectCount} direct, across ${value.items.length} Thread(s) shown${value.totalUnreadCount > shown ? `; ${value.totalUnreadCount - shown} more on Threads beyond this bounded list — call again with a larger limit.` : '.'}`,
153
+ ...value.items.map(item => `${item.threadRef}${item.channelRef === undefined ? '' : ` · ${item.channelRef}`}${item.taskRef === undefined ? '' : ` · ${taskStanding(item)}`} · ${item.unreadCount} unread, ${item.directCount} direct${item.newestOccurredAt === undefined ? '' : ` · newest ${formatTeamTimestamp(item.newestOccurredAt)}`}`),
154
+ 'Read a selected Thread with team_thread read. Listing changes no read state and supplies no write token.',
155
+ ].join('\n') }];
156
+ },
73
157
  },
74
158
  async execute(args, exec) {
75
159
  const agent = exec.agent;
@@ -78,7 +162,7 @@ const teamInbox = defineTool({
78
162
  const current = member(agent);
79
163
  const host = service(agent);
80
164
  const inbox = host.inboxForAgent(agent, { workspaceId: current.workspaceId, ...(args.limit === undefined ? {} : { limit: args.limit }) });
81
- const taskNumbers = new Map(host.viewForAgent(agent, { workspaceId: current.workspaceId, topLevelOnly: true, includeActivities: false })
165
+ const taskNumbers = new Map(host.viewForAgent(agent, { workspaceId: current.workspaceId, topLevelOnly: true, includeActivities: false, direction: 'before' })
82
166
  .taskNumbers.map(entry => [entry.taskRef, entry.taskNumber]));
83
167
  return {
84
168
  totalUnreadCount: inbox.totalUnreadCount, totalDirectCount: inbox.totalDirectCount,
@@ -86,7 +170,7 @@ const teamInbox = defineTool({
86
170
  const taskNumber = item.task === undefined ? undefined : taskNumbers.get(item.task.taskRef);
87
171
  return { threadRef: item.thread.threadRef, channelRef: item.channelRef,
88
172
  ...(item.task === undefined ? {} : { taskRef: item.task.taskRef, status: item.task.status }),
89
- revision: item.thread.revision, unreadCount: item.unreadCount, directCount: item.directCount,
173
+ revision: item.thread.revision, unreadCount: item.unreadCount, directCount: item.directCount, newestOccurredAt: item.newestOccurredAt,
90
174
  ...(taskNumber === undefined ? {} : { taskNumber }) };
91
175
  }),
92
176
  };
@@ -94,7 +178,7 @@ const teamInbox = defineTool({
94
178
  });
95
179
  const teamThread = defineTool({
96
180
  name: 'team_thread',
97
- description: 'Read or manage your Attention on one Thread. read acknowledges one chronological batch; history does not change read state. Prefer threadRef; taskRef is a compatibility alias when the Thread has a Task.',
181
+ description: 'Read or manage your Attention on one Thread. read acknowledges one chronological batch of unread facts and is the only read-side source of a next-write token — and only once no unread remains; your own committed public mutations hand off the token as well. history pages older facts without changing read state; status, follow, and unfollow change or report Attention only and render no Thread timeline. Prefer threadRef; taskRef is a compatibility alias when the Thread has a Task.',
98
182
  parameters: {
99
183
  action: { type: 'string', required: true, enum: ['status', 'follow', 'unfollow', 'read', 'history'] },
100
184
  threadRef: { type: 'string', description: "Full branded Thread ref exactly as returned by Team tools, including the 'thread:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
@@ -104,63 +188,92 @@ const teamThread = defineTool({
104
188
  output: {
105
189
  schema: { type: 'object', additionalProperties: false, properties: {
106
190
  kind: { type: 'string', required: true }, threadRef: { type: 'string', required: true }, taskRef: { type: 'string' },
107
- revision: { type: 'number', required: true }, status: { type: 'string' }, resolution: { type: 'string' },
191
+ revision: { type: 'number', required: true }, status: { type: 'string' }, resolution: { type: 'string' }, taskNumber: { type: 'number' },
108
192
  following: { type: 'boolean', required: true }, readThroughSequence: { type: 'number' }, remainingUnreadCount: { type: 'number' }, cursor: { type: 'number' }, hasMore: { type: 'boolean' },
109
193
  anchor: { type: 'object', required: true, additionalProperties: false, properties: {
110
- messageRef: { type: 'string', required: true }, sender: { type: 'string', required: true }, body: { type: 'string', required: true }, sequence: { type: 'number', required: true },
194
+ messageRef: { type: 'string', required: true }, sender: { type: 'string', required: true }, body: { type: 'string', required: true }, sequence: { type: 'number', required: true }, occurredAt: { type: 'string' },
111
195
  } },
112
196
  claims: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
113
197
  claimRef: { type: 'string', required: true }, direction: { type: 'string', required: true }, state: { type: 'string', required: true }, owner: { type: 'string', required: true },
114
198
  } } },
115
199
  facts: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
116
- sequence: { type: 'number', required: true }, kind: { type: 'string', required: true }, body: { type: 'string' }, sender: { type: 'string' }, mentions: { type: 'array', items: { type: 'string' } }, activity: { type: 'string' }, actor: { type: 'string' }, taskRef: { type: 'string' }, claimRef: { type: 'string' }, claimRefs: { type: 'array', items: { type: 'string' } }, completedClaimRefs: { type: 'array', items: { type: 'string' } }, acceptedClaimRefs: { type: 'array', items: { type: 'string' } }, releasedClaimRefs: { type: 'array', items: { type: 'string' } }, unread: { type: 'boolean' }, direct: { type: 'boolean' },
200
+ sequence: { type: 'number', required: true }, kind: { type: 'string', required: true }, body: { type: 'string' }, sender: { type: 'string' }, occurredAt: { type: 'string' }, mentions: { type: 'array', items: { type: 'string' } }, activity: { type: 'string' }, actor: { type: 'string' }, taskRef: { type: 'string' }, claimRef: { type: 'string' }, claimRefs: { type: 'array', items: { type: 'string' } }, completedClaimRefs: { type: 'array', items: { type: 'string' } }, acceptedClaimRefs: { type: 'array', items: { type: 'string' } }, releasedClaimRefs: { type: 'array', items: { type: 'string' } }, unread: { type: 'boolean' }, direct: { type: 'boolean' },
117
201
  } } },
118
202
  contextAdvice: { type: 'object', additionalProperties: false, properties: {
119
203
  usageTokens: { type: 'number' }, taskBoundaryThreshold: { type: 'number' }, handoffAt: { type: 'number' }, hardLimit: { type: 'number' },
120
204
  action: { type: 'string', required: true }, guidance: { type: 'string', required: true },
121
205
  } },
122
206
  } },
123
- // Renders are the only channel a tool result reaches the model through:
124
- // the header states the Thread ref and the Task's standing, each activity
125
- // line names the actor and every Claim the activity concluded, message
126
- // facts carry their unread/direct markers so a bounded batch can be
127
- // re-read discriminately, an acceptance the reader just acknowledged
128
- // carries one context-guidance section, and read/history footers state
129
- // what remains or whether older facts exist.
130
- render: (_args, value) => {
131
- // The header always identifies the Thread first — the ref the model
132
- // must echo in its next team_message reply then the Task's standing
133
- // when the Thread is taskful. An empty-facts status/follow result
134
- // still carries the same identifying surface.
135
- const header = [
136
- value.threadRef,
137
- value.taskRef === undefined ? '' : `${value.taskRef} · ${value.status}${value.resolution === undefined ? '' : `/${value.resolution}`}`,
138
- `revision ${value.revision}, following=${value.following}`,
139
- ].filter(part => part !== '').join(' · ');
140
- const lines = [header];
141
- // The Claims snapshot is the collision surface: another Member's
142
- // active Claim on this Task is invisible while facts alone render,
143
- // yet exactly what the model must see before claiming its own angle.
144
- for (const claim of value.claims)
145
- lines.push(`Claim ${claim.claimRef} · ${claim.state} ${claim.owner}: ${claim.direction}`);
146
- // The anchor is the Thread's root task statement. Render it whenever
147
- // it is not already among the facts, so a model reading a Thread for
148
- // the first time never loses the original ask.
149
- if (!value.facts.some(fact => fact.sequence === value.anchor.sequence))
150
- lines.push(`Anchor ${value.anchor.sequence} [${value.anchor.sender}] ${value.anchor.body}`);
151
- for (const fact of value.facts) {
152
- lines.push(fact.kind === 'message'
153
- ? `${fact.sequence} [${fact.sender ?? 'unknown sender'}]${fact.unread === undefined ? '' : fact.direct === true ? ' [direct]' : fact.unread === true ? ' [unread]' : ''} ${fact.body}`
154
- : activityLine(fact));
155
- if (fact.kind !== 'message' && fact.unread === true)
156
- lines.push(`${fact.sequence} … (unread activity)`);
207
+ // Renders are the only channel a tool result reaches the model through.
208
+ // status/follow/unfollow answer only the Attention question. read renders
209
+ // outcome context orientation active collision surface facts →
210
+ // watermark/advice, and hands off the opaque next-write token only when
211
+ // no unread remains. history renders deep orientation and never the
212
+ // current collision surface or any write token.
213
+ render: (args, value) => {
214
+ const standing = taskStanding(value);
215
+ if (value.kind === 'status') {
216
+ return [{ type: 'text', text: `Attention status${value.following ? 'following' : 'not following'} ${value.threadRef}${standing === '' ? '' : ` · ${standing}`}.` }];
217
+ }
218
+ if (value.kind === 'follow' || value.kind === 'unfollow') {
219
+ return [{ type: 'text', text: `Attention changed — ${value.following ? 'now following' : 'no longer following'} ${value.threadRef}${standing === '' ? '' : ` · ${standing}`}.` }];
220
+ }
221
+ if (value.kind === 'history') {
222
+ const facts = value.facts;
223
+ const lines = [`History for ${value.threadRef}${standing === '' ? '' : ` · ${standing}`}`];
224
+ // First page (no beforeSequence supplied) orients on the full
225
+ // anchor; a continuation orients on the shared bounded subject. An
226
+ // anchor already selected as a fact never repeats.
227
+ if (!facts.some(fact => fact.sequence === value.anchor.sequence)) {
228
+ if (args.beforeSequence === undefined)
229
+ lines.push('', `Anchor ${value.anchor.sequence}${value.anchor.occurredAt === undefined ? '' : ` ${formatTeamTimestamp(value.anchor.occurredAt)}`} [${value.anchor.sender}]`, value.anchor.body);
230
+ else
231
+ lines.push(` — ${boundedSubject(value.anchor.body)}`);
232
+ }
233
+ lines.push('', 'Facts');
234
+ if (facts.length === 0)
235
+ lines.push('No facts before this cursor.');
236
+ else
237
+ lines.push(...facts.map(fact => factLine(fact)));
238
+ lines.push('', `History cursor ${value.cursor}; hasMore=${value.hasMore ? 'true' : 'false'}${value.hasMore ? ' — older facts exist; page again with beforeSequence set to the cursor.' : ' — no older facts remain.'}`);
239
+ return [{ type: 'text', text: lines.join('\n') }];
240
+ }
241
+ // kind === 'read': outcome → context → orientation → collision
242
+ // surface → facts → watermark → token (iff nothing unread remains).
243
+ const facts = value.facts;
244
+ const acknowledged = facts.filter(fact => fact.unread === true).length;
245
+ const remaining = value.remainingUnreadCount ?? 0;
246
+ const lines = [
247
+ `Read committed — ${acknowledged === 0 ? `no unread updates on ${value.threadRef}; nothing remains` : `acknowledged ${acknowledged} unread update(s) on ${value.threadRef}; ${remaining} remain`}.`,
248
+ `${value.threadRef}${standing === '' ? '' : ` · ${standing}`} · ${value.following ? 'following' : 'not following'}`,
249
+ ];
250
+ // Orientation: A the anchor is one of the returned facts (renders as
251
+ // that fact once, no separate anchor); B any Host-supplied background
252
+ // fact carries positive `unread === false`, so the full anchor renders
253
+ // before the facts; C otherwise (continuation or unfollowed ad-hoc
254
+ // read) the shared bounded subject — never the full anchor.
255
+ const anchorInFacts = facts.some(fact => fact.sequence === value.anchor.sequence);
256
+ const hasBackground = facts.some(fact => fact.unread === false);
257
+ if (!anchorInFacts && hasBackground)
258
+ lines.push('', `Anchor ${value.anchor.sequence}${value.anchor.occurredAt === undefined ? '' : ` ${formatTeamTimestamp(value.anchor.occurredAt)}`} [${value.anchor.sender}]`, value.anchor.body);
259
+ if (!anchorInFacts && !hasBackground)
260
+ lines.push(` — ${boundedSubject(value.anchor.body)}`);
261
+ const activeClaims = value.claims.filter(claim => claim.state === 'active');
262
+ if (activeClaims.length > 0) {
263
+ lines.push('', 'Active Claims');
264
+ for (const claim of activeClaims)
265
+ lines.push(claimLine(claim));
157
266
  }
158
- if (value.kind === 'read')
159
- lines.push(`Read through sequence ${value.readThroughSequence}; ${value.remainingUnreadCount ?? 0} unread update(s) remaining — call team_thread read again${(value.remainingUnreadCount ?? 0) > 0 ? '' : ' when new work arrives'}.`);
160
- if (value.kind === 'history')
161
- lines.push(`History cursor ${value.cursor}; hasMore=${value.hasMore ? 'true' : 'false'}${value.hasMore ? ' — older facts exist; page again with beforeSequence set to the cursor.' : ' — no older facts remain.'}`);
162
- if (value.kind === 'read' && value.contextAdvice !== undefined)
163
- lines.push(...adviceLines(value.contextAdvice));
267
+ lines.push('', 'Facts');
268
+ if (facts.length === 0)
269
+ lines.push('No new facts to acknowledge.');
270
+ else
271
+ lines.push(...facts.map(fact => factLine(fact)));
272
+ lines.push('', `Read through sequence ${value.readThroughSequence}; ${remaining} unread update(s) remaining${remaining > 0 ? ' — call team_thread read again.' : '.'}`);
273
+ if (value.contextAdvice !== undefined)
274
+ lines.push('', ...adviceLines(value.contextAdvice));
275
+ if (remaining === 0)
276
+ lines.push('', nextWriteLine(value.revision));
164
277
  return [{ type: 'text', text: lines.join('\n') }];
165
278
  },
166
279
  },
@@ -173,44 +286,62 @@ const teamThread = defineTool({
173
286
  if (args.threadRef === undefined && args.taskRef === undefined)
174
287
  throw new Error('team_thread requires threadRef');
175
288
  const base = { workspaceId: current.workspaceId, ...(args.threadRef === undefined ? {} : { threadRef: args.threadRef }), ...(args.taskRef === undefined ? {} : { taskRef: args.taskRef }) };
289
+ const taskNumberOf = (task) => {
290
+ if (task === undefined)
291
+ return {};
292
+ const resolved = host.resolveTaskRefs({ workspaceId: current.workspaceId, taskRefs: [task.taskRef] }).resolved[0];
293
+ return resolved === undefined ? {} : { taskNumber: resolved.taskNumber };
294
+ };
176
295
  if (args.action === 'status') {
177
296
  if (args.beforeSequence !== undefined || args.limit !== undefined)
178
297
  throw new Error('status does not accept history arguments');
179
298
  const status = host.attentionStatusForAgent(agent, base);
180
299
  const snapshot = host.threadHistoryForAgent(agent, { ...base, beforeSequence: 1, limit: 1 });
181
- return threadResult('status', snapshot, status.attention, []);
300
+ return threadResult('status', snapshot, status.attention, [], taskNumberOf(snapshot.task));
182
301
  }
183
302
  if (args.action === 'follow' || args.action === 'unfollow') {
184
303
  if (args.beforeSequence !== undefined || args.limit !== undefined)
185
304
  throw new Error(`${args.action} does not accept history arguments`);
186
305
  const result = await host.changeAttentionForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base, action: args.action });
187
306
  const snapshot = host.threadHistoryForAgent(agent, { ...base, beforeSequence: 1, limit: 1 });
188
- return threadResult(args.action, snapshot, result.attention, []);
307
+ return threadResult(args.action, snapshot, result.attention, [], taskNumberOf(snapshot.task));
189
308
  }
190
309
  if (args.action === 'history') {
191
310
  const history = host.threadHistoryForAgent(agent, { ...base, ...(args.beforeSequence === undefined ? {} : { beforeSequence: args.beforeSequence }), ...(args.limit === undefined ? {} : { limit: args.limit }) });
192
311
  const status = host.attentionStatusForAgent(agent, base);
193
312
  return threadResult('history', history, status.attention, history.facts.map(fact => fact.kind === 'message'
194
- ? { sequence: fact.sequence, kind: 'message', body: fact.message.body, sender: fact.message.sender, mentions: [...fact.mentions] }
195
- : activityFactView(fact.sequence, fact.activity)), { cursor: history.cursor, hasMore: history.hasMore });
313
+ ? { sequence: fact.sequence, kind: 'message', body: fact.message.body, sender: fact.message.sender, mentions: [...fact.mentions], occurredAt: fact.occurredAt }
314
+ : activityFactView(fact.sequence, fact.activity, undefined, fact.occurredAt)), { cursor: history.cursor, hasMore: history.hasMore, ...taskNumberOf(history.task) });
196
315
  }
197
316
  if (args.beforeSequence !== undefined || args.limit !== undefined)
198
317
  throw new Error('read does not accept history arguments');
199
318
  const read = await host.readThreadForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base });
200
319
  return threadResult('read', read, read.attention, read.facts.map(entry => entry.fact.kind === 'message'
201
- ? { sequence: entry.fact.sequence, kind: 'message', body: entry.fact.message.body, sender: entry.fact.message.sender, mentions: [...entry.fact.mentions], unread: entry.unread, direct: entry.direct }
202
- : activityFactView(entry.fact.sequence, entry.fact.activity, { unread: entry.unread, direct: entry.direct })), { readThroughSequence: read.readThroughSequence, remainingUnreadCount: read.remainingUnreadCount, ...(read.contextAdvice === undefined ? {} : { contextAdvice: adviceView(read.contextAdvice) }) });
320
+ ? { sequence: entry.fact.sequence, kind: 'message', body: entry.fact.message.body, sender: entry.fact.message.sender, mentions: [...entry.fact.mentions], unread: entry.unread, direct: entry.direct, occurredAt: entry.fact.occurredAt }
321
+ : activityFactView(entry.fact.sequence, entry.fact.activity, { unread: entry.unread, direct: entry.direct }, entry.fact.occurredAt)), { readThroughSequence: read.readThroughSequence, remainingUnreadCount: read.remainingUnreadCount, ...(read.contextAdvice === undefined ? {} : { contextAdvice: adviceView(read.contextAdvice) }), ...taskNumberOf(read.task) });
203
322
  },
204
323
  });
324
+ /** One rendered fact line; markers are inline, never a separate ellipsis line. */
325
+ function factLine(fact) {
326
+ const at = fact.occurredAt === undefined ? '' : ` ${formatTeamTimestamp(fact.occurredAt)}`;
327
+ return fact.kind === 'message'
328
+ ? `${fact.sequence}${at} [${fact.sender ?? 'unknown sender'}]${factMarkers(fact)} ${fact.body}`
329
+ : `${activityLine(fact)}${at}${factMarkers(fact)}`;
330
+ }
331
+ /** One collision-surface Claim line, shared by read and history-free listing. */
332
+ function claimLine(claim) {
333
+ return `${claim.claimRef} — ${claim.owner}: ${claim.direction}`;
334
+ }
205
335
  function threadResult(kind, snapshot, attention, facts, extra = {}) {
206
336
  return {
207
337
  kind, threadRef: snapshot.thread.threadRef, revision: snapshot.thread.revision,
208
338
  ...(snapshot.task === undefined ? {} : { taskRef: snapshot.task.taskRef, status: snapshot.task.status, resolution: snapshot.task.resolution }),
339
+ ...(extra.taskNumber === undefined ? {} : { taskNumber: extra.taskNumber }),
209
340
  following: attention !== undefined,
210
341
  ...extra,
211
342
  ...(attention === undefined || extra.readThroughSequence !== undefined ? {} : { readThroughSequence: attention.readThroughSequence }),
212
- anchor: { messageRef: snapshot.anchor.messageRef, sender: snapshot.anchor.sender, body: snapshot.anchor.body, sequence: snapshot.anchor.sequence },
213
- claims: snapshot.claims.map(claim => ({ claimRef: claim.claimRef, direction: claim.direction, state: claim.state, owner: claim.owner })),
343
+ anchor: { messageRef: snapshot.anchor.messageRef, sender: snapshot.anchor.sender, body: snapshot.anchor.body, sequence: snapshot.anchor.sequence, occurredAt: snapshot.anchor.occurredAt },
344
+ claims: snapshot.claims.map(claimView),
214
345
  facts,
215
346
  };
216
347
  }
@@ -224,7 +355,7 @@ function adviceLines(advice) {
224
355
  }
225
356
  const teamMessage = markAgentTeamPreset(defineTool({
226
357
  name: 'team_message',
227
- description: 'Start a top-level Thread, reply to an existing Thread, or send a direct message (DM). start defaults to a taskless Thread; pass asTask true to create a Task in the same send. Read the Thread first; replies require its current revision (an internal concurrency token carried by baseRevision, never quoted in bodies). A top-level start may mention related Agents directly; in replies, only a Human can invite an unfollowed Agent. Pass Member refs in mentions and spell their handles inside the body; only mentioned Members render as mention chips. dm sends a private direct message to one enabled Agent Member in your Workspace: use it for quick clarifications and status syncs — never for task work, decisions, or anything that needs team visibility or traceability (use a Thread); if a DM exchange with the same Member exceeds about 3 exchanges, move it to a Thread, because every DM costs the recipient a full agent turn.',
358
+ description: 'Start a top-level Thread, reply to an existing Thread, or send a direct message (DM). Read the Thread first: a reply needs the current next-write token from a fully drained team_thread read (or your own last committed mutation), and unread work rejects before staleness is even checked. start defaults to a taskless Thread; pass asTask true to create a Task in the same send. A top-level start may mention related Agents directly; in replies, only a Human can invite an unfollowed Agent. Pass Member refs in mentions and spell their handles inside the body; only mentioned Members render as mention chips. dm sends a private direct message to one enabled Agent Member in your Workspace: use it for quick clarifications and status syncs — never for task work, decisions, or anything that needs team visibility or traceability (use a Thread); if a DM exchange with the same Member exceeds about 3 exchanges, move it to a Thread, because every DM costs the recipient a full agent turn.',
228
359
  parameters: {
229
360
  action: { type: 'string', required: true, enum: ['start', 'reply', 'dm'] },
230
361
  channelRef: { type: 'string', description: "Full branded Channel ref exactly as returned by Team tools, including the 'channel:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
@@ -232,22 +363,36 @@ const teamMessage = markAgentTeamPreset(defineTool({
232
363
  taskRef: { type: 'string', description: "Optional Task ref alias for reply on a Taskful Thread. Prefer threadRef; an unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
233
364
  memberRef: { type: 'string', description: "Full branded Member ref exactly as returned by Team tools, including the 'member:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves. Required for dm; the Member must be an enabled Agent in your Workspace (the Human cannot be DMed)." },
234
365
  asTask: { type: 'boolean', description: 'When true, start creates a Task with the Thread. Default false creates a taskless Thread.' },
235
- body: { type: 'string', required: true, description: "Markdown body. Cite Team refs exactly as returned, as bare text with one colon (e.g. task:0f0a…) — never a double colon, never inside backticks or quotes. Unambiguous UUID abbreviations (first 6+ hex chars) also resolve. Spell each mentioned Member's handle in the prose so the mention renders inline." }, baseRevision: { type: 'number', description: "Positive integer; use the current Thread revision as shown by the latest team_inbox or team_thread result for this Thread. The revision is an internal concurrency token, not a citable fact." },
366
+ body: { type: 'string', required: true, description: "Markdown body. Cite Team refs exactly as returned, as bare text with one colon (e.g. task:0f0a…) — never a double colon, never inside backticks or quotes. Unambiguous UUID abbreviations (first 6+ hex chars) also resolve. Spell each mentioned Member's handle in the prose so the mention renders inline." }, baseRevision: { type: 'number', description: "The next-write token from your latest fully drained team_thread read (or your own last committed mutation) on this Thread. Copy the explicitly rendered value verbatim; never increment, derive, compare, or cite it — it is an opaque concurrency token, not a fact about the Thread." },
236
367
  mentions: { type: 'array', items: { type: 'string' }, description: 'Member refs to mention. Mentioned Agents receive the Message directly; write their handles in the body (any casing, optional @) so the mention renders inline.' },
237
368
  attachments: { type: 'array', items: { type: 'string' }, description: 'Absolute file paths to share, e.g. screenshots or generated artifacts; images render as thumbnails for recipients. The Host validates each path and copies the file into the attachment cache, and members also receive one cached path per attachment; if any path fails validation the whole send is rejected.' },
238
369
  },
239
370
  output: {
240
371
  schema: { type: 'object', additionalProperties: false, properties: {
241
- kind: { type: 'string', required: true }, taskRef: { type: 'string' }, threadRef: { type: 'string' }, revision: { type: 'number' },
372
+ kind: { type: 'string', required: true }, action: { type: 'string' }, taskRef: { type: 'string' }, threadRef: { type: 'string' }, revision: { type: 'number' },
242
373
  expectedRevision: { type: 'number' }, messageRef: { type: 'string' }, memberIds: { type: 'array', items: { type: 'string' } }, unreadCount: { type: 'number' }, directCount: { type: 'number' },
243
- recipientMemberId: { type: 'string' }, recipientHandle: { type: 'string' }, delivered: { type: 'boolean' }, deliveryNote: { type: 'string' },
374
+ recipientMemberId: { type: 'string' }, recipientHandle: { type: 'string' }, delivered: { type: 'boolean' }, deliveryNote: { type: 'string' }, occurredAt: { type: 'string' },
244
375
  } },
245
- render: (_args, value) => [{ type: 'text', text: value.kind === 'dm-sent' ? `DM ${value.delivered === false ? 'recorded but not delivered' : 'delivered'} to @${value.recipientHandle} (${value.recipientMemberId})${value.deliveryNote === undefined ? '' : `: ${value.deliveryNote}`}`
246
- : value.kind === 'committed' ? `Message ${value.messageRef} committed at revision ${value.revision} on ${value.threadRef}${value.taskRef === undefined ? '' : ` (${value.taskRef})`}.`
247
- : value.kind === 'unread_required' ? `unread_required: ${value.threadRef}${value.taskRef === undefined ? '' : ` (${value.taskRef})`} has ${value.unreadCount} unread update(s), ${value.directCount} direct at revision ${value.revision}. Read the pending updates (team_thread read) before retrying this send.`
248
- : value.kind === 'stale_revision' ? `stale_revision: your baseRevision ${value.expectedRevision} is obsolete; ${value.threadRef}${value.taskRef === undefined ? '' : ` (${value.taskRef})`} is now at revision ${value.revision}. Read the Thread, then retry with baseRevision ${value.revision}.`
249
- : value.kind === 'member_not_following' ? `member_not_following: ${(value.memberIds ?? []).join(', ')} not following; the message was not committed. Only a Human can invite an unfollowed Agent — retry without mentioning them, or ask the Human.`
250
- : `${value.kind}: ${value.memberIds?.join(', ') ?? `${value.threadRef ?? ''}${value.taskRef === undefined ? '' : ` · Task ${value.taskRef}`} revision ${value.revision ?? ''}`}` }],
376
+ render: (_args, value) => {
377
+ if (value.kind === 'dm-sent') {
378
+ if (value.delivered === false)
379
+ return [{ type: 'text', text: [
380
+ `Recorded, not delivered DM to @${value.recipientHandle} (${value.recipientMemberId}).`,
381
+ 'No automatic redelivery will occur; do not blindly send a duplicate.',
382
+ `Reason: ${value.deliveryNote ?? 'the recipient session could not be woken'}`,
383
+ ].join('\n') }];
384
+ return [{ type: 'text', text: `Delivered — DM to @${value.recipientHandle} (${value.recipientMemberId})${value.occurredAt === undefined ? '' : ` at ${formatTeamTimestamp(value.occurredAt)}`}.` }];
385
+ }
386
+ if (value.kind === 'committed' && value.messageRef !== undefined && value.threadRef !== undefined && value.revision !== undefined) {
387
+ return [{ type: 'text', text: [
388
+ value.action === 'start' ? 'Committed — Thread created.' : 'Committed — reply added.',
389
+ [value.messageRef, value.threadRef, ...(value.taskRef === undefined ? [] : [value.taskRef])].join(' · '),
390
+ ...(value.occurredAt === undefined ? [] : [`Committed at ${formatTeamTimestamp(value.occurredAt)}`]),
391
+ nextWriteLine(value.revision),
392
+ ].join('\n') }];
393
+ }
394
+ return [{ type: 'text', text: rejectionLines('message', value).join('\n') }];
395
+ },
251
396
  // Minimal durable projection for the Host's context timeline: the
252
397
  // structured outcome identity (never the render text). The effect-anchor
253
398
  // fold reads `kind === 'committed'` + threadRef from the persisted
@@ -271,7 +416,7 @@ const teamMessage = markAgentTeamPreset(defineTool({
271
416
  throw new Error('start requires channelRef and does not accept threadRef, taskRef, or baseRevision');
272
417
  const result = await host.sendMessageForAgent(agent, { requestId: requestId(agent.id, exec.callId), workspaceId: current.workspaceId,
273
418
  channelRef: args.channelRef, body: args.body, asTask: args.asTask === true, ...(mentions === undefined ? {} : { recipients: mentions }), ...paths });
274
- return messageOutcome(result);
419
+ return messageOutcome(result, 'start');
275
420
  }
276
421
  if (args.action === 'dm') {
277
422
  if (args.memberRef === undefined || args.channelRef !== undefined || args.threadRef !== undefined || args.taskRef !== undefined
@@ -281,7 +426,7 @@ const teamMessage = markAgentTeamPreset(defineTool({
281
426
  try {
282
427
  const result = await host.dmForAgent(agent, { requestId: requestId(agent.id, exec.callId), workspaceId: current.workspaceId,
283
428
  recipientMemberId: args.memberRef, body: args.body });
284
- return { kind: 'dm-sent', recipientMemberId: result.recipient.memberId, recipientHandle: result.recipient.handle, delivered: true };
429
+ return { kind: 'dm-sent', recipientMemberId: result.recipient.memberId, recipientHandle: result.recipient.handle, delivered: true, occurredAt: result.receipt.occurredAt };
285
430
  }
286
431
  catch (error) {
287
432
  if (error instanceof AgentTeamDmDeliveryError) {
@@ -292,21 +437,21 @@ const teamMessage = markAgentTeamPreset(defineTool({
292
437
  }
293
438
  const baseRevision = args.baseRevision;
294
439
  if ((args.threadRef === undefined && args.taskRef === undefined) || args.channelRef !== undefined || args.asTask !== undefined || typeof baseRevision !== 'number' || !Number.isSafeInteger(baseRevision) || baseRevision < 1) {
295
- throw new Error("reply requires threadRef and a positive baseRevision, and does not accept channelRef; use the current Thread 'revision' returned by team_inbox or team_thread");
440
+ throw new Error('reply requires threadRef and a positive baseRevision; drain the Thread with team_thread read and copy the token it renders, or reuse the one your own last committed mutation rendered');
296
441
  }
297
442
  const result = await host.replyForAgent(agent, { requestId: requestId(agent.id, exec.callId), workspaceId: current.workspaceId,
298
443
  ...(args.threadRef === undefined ? {} : { threadRef: args.threadRef }),
299
444
  ...(args.taskRef === undefined ? {} : { taskRef: args.taskRef }),
300
445
  body: args.body, baseRevision,
301
446
  ...(mentions === undefined ? {} : { recipients: mentions }), ...paths });
302
- return messageOutcome(result);
447
+ return messageOutcome(result, 'reply');
303
448
  },
304
449
  }));
305
- function messageOutcome(result) {
450
+ function messageOutcome(result, action) {
306
451
  if (result.kind === 'committed')
307
- return { kind: result.kind, threadRef: result.thread.threadRef,
452
+ return { kind: result.kind, action, threadRef: result.thread.threadRef,
308
453
  ...(result.task === undefined ? {} : { taskRef: result.task.taskRef }),
309
- revision: result.thread.revision, messageRef: result.message.messageRef };
454
+ revision: result.thread.revision, messageRef: result.message.messageRef, occurredAt: result.receipt.occurredAt };
310
455
  if (result.kind === 'member_not_following')
311
456
  return { kind: result.kind, memberIds: [...result.memberIds],
312
457
  ...(result.taskRef === undefined ? {} : { taskRef: result.taskRef }), ...(result.threadRef === undefined ? {} : { threadRef: result.threadRef, revision: result.revision }) };
@@ -320,28 +465,48 @@ function messageOutcome(result) {
320
465
  }
321
466
  const teamClaim = defineTool({
322
467
  name: 'team_claim',
323
- description: 'List or mutate your Direction Claims. Read the Thread first; every mutation uses the current Thread revision. A Claim is your one-sentence direction statement on a Task — "the angle I am taking" — so others can spot collisions and track progress: Tasks define scope (owned by Humans), Claims declare the angle (owned by you). Good direction: "Unify the four form dialogs on shared field components before wiring submits." Bad direction: a multi-paragraph plan with step order, file lists, or acceptance criteria — those belong in Thread messages, not the Claim.',
468
+ description: 'List or mutate your Direction Claims. A Claim is your one-sentence direction statement on a Task — "the angle I am taking" — so others can spot collisions and track progress: Tasks define scope (owned by Humans), Claims declare the angle (owned by you). Good direction: "Unify the four form dialogs on shared field components before wiring submits." Bad direction: a multi-paragraph plan with step order, file lists, or acceptance criteria — those belong in Thread messages, not the Claim. Mutations require the current next-write token from a fully drained team_thread read (or your own last committed mutation); list refreshes the collision surface only and authorizes no mutation.',
324
469
  parameters: {
325
470
  action: { type: 'string', required: true, enum: ['list', 'claim', 'done', 'release'] },
326
471
  taskRef: { type: 'string', required: true, description: "Full branded Task ref exactly as returned by Team tools, including the 'task:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
327
- baseRevision: { type: 'number', description: "Positive integer; use the current Thread revision as shown by the latest team_inbox or team_thread result for this Task. The revision is an internal concurrency token, not a citable fact." }, direction: { type: 'string' },
472
+ baseRevision: { type: 'number', description: "The next-write token from your latest fully drained team_thread read (or your own last committed mutation) on this Task's Thread. Copy the explicitly rendered value verbatim; never increment, derive, compare, or cite it — it is an opaque concurrency token, not a fact about the Task." }, direction: { type: 'string' },
328
473
  claimRef: { type: 'string', description: "Full branded Claim ref exactly as returned by team_claim, including the 'claim:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
329
474
  },
330
475
  output: {
331
476
  schema: { type: 'object', additionalProperties: false, properties: {
332
- kind: { type: 'string', required: true }, taskRef: { type: 'string', required: true }, threadRef: { type: 'string', required: true },
477
+ kind: { type: 'string', required: true }, action: { type: 'string' }, taskRef: { type: 'string', required: true }, threadRef: { type: 'string', required: true },
333
478
  revision: { type: 'number', required: true }, expectedRevision: { type: 'number' }, status: { type: 'string', required: true },
334
- unreadCount: { type: 'number' }, directCount: { type: 'number' },
479
+ unreadCount: { type: 'number' }, directCount: { type: 'number' }, occurredAt: { type: 'string' },
480
+ claim: { type: 'object', additionalProperties: false, properties: {
481
+ claimRef: { type: 'string', required: true }, direction: { type: 'string', required: true }, state: { type: 'string', required: true }, owner: { type: 'string', required: true },
482
+ } },
335
483
  claims: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: { claimRef: { type: 'string', required: true }, direction: { type: 'string', required: true }, state: { type: 'string', required: true }, owner: { type: 'string', required: true } } } },
336
484
  } },
337
- render: (_args, value) => [{ type: 'text', text: [
338
- value.kind === 'unread_required'
339
- ? `unread_required: ${value.taskRef} (${value.threadRef}) has ${value.unreadCount} unread update(s), ${value.directCount} direct at revision ${value.revision}. Read the pending updates (team_thread read) before retrying this Claim mutation.`
340
- : value.kind === 'stale_revision'
341
- ? `stale_revision: your baseRevision ${value.expectedRevision} is obsolete; ${value.taskRef} (${value.threadRef}) is now at revision ${value.revision}. Read the Thread, then retry with baseRevision ${value.revision}.`
342
- : `${value.kind}: ${value.taskRef} (${value.threadRef}) · ${value.status}, revision ${value.revision}`,
343
- ...value.claims.map(claim => `${claim.claimRef} · ${claim.state} — ${claim.owner}: ${claim.direction}`)
344
- ].join('\n') }],
485
+ // A committed mutation renders the authoritative affected Claim first —
486
+ // the Host result carries it; the full archive is never appended. list
487
+ // renders the active collision surface and no write token: a current
488
+ // Thread read remains the required mutation basis. Rejections share the
489
+ // outcome-first recovery form and never a numeric revision.
490
+ render: (_args, value) => {
491
+ if (value.kind === 'listed') {
492
+ const active = value.claims.filter(claim => claim.state === 'active');
493
+ return [{ type: 'text', text: [
494
+ `Claims for ${value.taskRef} · ${value.threadRef} · ${value.status}`,
495
+ 'Active Claims',
496
+ ...(active.length === 0 ? ['No active Claims.'] : active.map(claim => claimLine(claim))),
497
+ ].join('\n') }];
498
+ }
499
+ if (value.kind === 'committed' && value.claim !== undefined) {
500
+ return [{ type: 'text', text: [
501
+ `Committed — Claim ${value.action === 'claim' ? 'created' : value.action === 'done' ? 'completed' : 'released'}.`,
502
+ `${value.claim.claimRef} · ${value.claim.state} — ${value.claim.owner}: ${value.claim.direction}`,
503
+ `${value.threadRef} · ${value.taskRef} · ${value.status}`,
504
+ ...(value.occurredAt === undefined ? [] : [`Committed at ${formatTeamTimestamp(value.occurredAt)}`]),
505
+ nextWriteLine(value.revision),
506
+ ].join('\n') }];
507
+ }
508
+ return [{ type: 'text', text: rejectionLines('Claim mutation', value).join('\n') }];
509
+ },
345
510
  },
346
511
  async execute(args, exec) {
347
512
  const agent = exec.agent;
@@ -355,22 +520,25 @@ const teamClaim = defineTool({
355
520
  throw new Error('list accepts only taskRef');
356
521
  const listed = host.listClaimsForAgent(agent, base);
357
522
  return { kind: 'listed', taskRef: listed.task.taskRef, threadRef: listed.thread.threadRef, revision: listed.thread.revision, status: listed.task.status,
358
- claims: listed.claims.map(claim => ({ claimRef: claim.claimRef, owner: claim.owner, direction: claim.direction, state: claim.state })) };
523
+ claims: listed.claims.map(claimView) };
359
524
  }
360
525
  const baseRevision = args.baseRevision;
361
526
  if (typeof baseRevision !== 'number' || !Number.isSafeInteger(baseRevision) || baseRevision < 1)
362
- throw new Error("claim mutation requires a positive baseRevision; use the current Thread 'revision' returned by team_inbox or team_thread for this Task");
527
+ throw new Error('claim mutation requires a positive baseRevision; drain the Thread with team_thread read and copy the token it renders, or reuse the one your own last committed mutation rendered');
363
528
  if (args.action === 'claim' && (args.direction === undefined || args.claimRef !== undefined))
364
529
  throw new Error('claim requires direction and does not accept claimRef');
365
530
  if ((args.action === 'done' || args.action === 'release') && (args.claimRef === undefined || args.direction !== undefined))
366
531
  throw new Error(`${args.action} requires claimRef and does not accept direction`);
367
532
  const result = await host.changeClaimForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base, action: args.action,
368
533
  baseRevision, ...(args.direction === undefined ? {} : { direction: args.direction }), ...(args.claimRef === undefined ? {} : { claimRef: args.claimRef }) });
534
+ // Structured compatibility: every mutation outcome re-reads the real
535
+ // Claim archive and Task status, exactly as the parent version did —
536
+ // the render layer is what omits the archive, never the structured value.
369
537
  const listed = host.listClaimsForAgent(agent, base);
370
- const claims = listed.claims.map(claim => ({ claimRef: claim.claimRef, owner: claim.owner, direction: claim.direction, state: claim.state }));
538
+ const claims = listed.claims.map(claimView);
371
539
  if (result.kind === 'committed')
372
- return { kind: result.kind, taskRef: result.task.taskRef, threadRef: result.thread.threadRef,
373
- revision: result.thread.revision, status: result.task.status, claims };
540
+ return { kind: result.kind, action: args.action, taskRef: result.task.taskRef, threadRef: result.thread.threadRef,
541
+ revision: result.thread.revision, status: result.task.status, claim: claimView(result.claim), claims, occurredAt: result.receipt.occurredAt };
374
542
  if (result.kind === 'unread_required')
375
543
  return { kind: result.kind, taskRef: listed.task.taskRef, threadRef: result.threadRef,
376
544
  revision: result.revision, status: listed.task.status, unreadCount: result.unreadCount, directCount: result.directCount, claims };
@@ -380,7 +548,7 @@ const teamClaim = defineTool({
380
548
  });
381
549
  const teamView = defineTool({
382
550
  name: 'team_view',
383
- description: 'Discover authorized Team Channels, top-level Threads, Tasks, and Members. It is not a substitute for team_thread reading.',
551
+ description: 'Discover your authorized Team addresses: current Channels, a newest-first page of top-level Threads (each with its bounded anchor subject; Task standing inline on taskful rows), and current Members. This is an address book, not a work queue — unread work lives in team_inbox, and a Thread is read with team_thread read. The cursor pages Thread rows only.',
384
552
  parameters: {
385
553
  channelRef: { type: 'string', description: "Full branded Channel ref exactly as returned by Team tools, including the 'channel:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
386
554
  limit: { type: 'number' }, cursor: { type: 'number' },
@@ -392,23 +560,45 @@ const teamView = defineTool({
392
560
  memberId: { type: 'string', required: true }, kind: { type: 'string', required: true }, handle: { type: 'string', required: true }, description: { type: 'string', required: true }, presence: { type: 'string', required: true },
393
561
  } } },
394
562
  threads: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
395
- threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true }, revision: { type: 'number', required: true }, messageCount: { type: 'number', required: true },
396
- taskRef: { type: 'string' }, status: { type: 'string' }, taskNumber: { type: 'number' },
563
+ threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true }, revision: { type: 'number', required: true }, messageCount: { type: 'number', required: true }, subject: { type: 'string', required: true },
564
+ taskRef: { type: 'string' }, status: { type: 'string' }, taskNumber: { type: 'number' }, lastActivityAt: { type: 'string' },
397
565
  } } },
398
566
  tasks: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: { taskRef: { type: 'string', required: true }, threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true }, status: { type: 'string', required: true }, revision: { type: 'number', required: true } } } },
399
- cursor: { type: 'number', required: true }, hasMore: { type: 'boolean', required: true },
567
+ cursor: { type: 'number', required: true }, hasMore: { type: 'boolean', required: true }, page: { type: 'string' },
400
568
  } },
401
- render: (_args, value) => [{ type: 'text', text: [
402
- ...value.channels.map(channel => `${channel.channelRef} · ${channel.name}`),
403
- ...value.members.map(m => `${m.memberId} · ${m.handle} (${m.kind}, ${m.presence})${m.description === '' ? '' : ` — ${m.description}`}`),
404
- ...(value.threads.length > 0
405
- ? value.threads.map(thread => `${thread.threadRef} · ${thread.channelRef}${thread.taskRef === undefined ? '' : ` · ${thread.taskRef}${thread.taskNumber === undefined ? '' : ` (#${thread.taskNumber})`} (${thread.status})`} · ${thread.messageCount} message(s), revision ${thread.revision}`)
406
- : ['No Team Threads.']),
407
- ...(value.tasks.length > 0
408
- ? value.tasks.map(task => `${task.taskRef} · ${task.threadRef} · ${task.channelRef} · ${task.status}, revision ${task.revision}`)
409
- : ['No Team Tasks.']),
410
- `cursor ${value.cursor}, hasMore=${value.hasMore ? 'true' : 'false'}${value.hasMore ? ' — more items exist; call team_view again with cursor set to this value.' : ' — no further pages.'}`,
411
- ].join('\n') }],
569
+ // Address book, newest Thread first: labelled sections, one bounded
570
+ // anchor subject per Thread row, Task standing inline on its Thread (no
571
+ // second Task index), and a footer that calls the value a Thread cursor.
572
+ // Continuation pages repeat only Threads — Channels and Members are
573
+ // current address context, not members of the Thread page. No message
574
+ // count and no revision label: neither changes the next legal action,
575
+ // which is reading, never writing from a directory snapshot.
576
+ render: (_args, value) => {
577
+ const continuation = value.page === 'threads';
578
+ const lines = [];
579
+ if (!continuation) {
580
+ lines.push('Team directory', '', 'Channels — current');
581
+ if (value.channels.length === 0)
582
+ lines.push('No authorized Channels.');
583
+ else
584
+ lines.push(...value.channels.map(channel => `${channel.channelRef} · ${channel.name}`));
585
+ lines.push('');
586
+ }
587
+ lines.push('Threads');
588
+ if (value.threads.length === 0)
589
+ lines.push(`No top-level Threads${!continuation && value.channels.length === 1 ? ` in ${value.channels[0].channelRef}` : ''} at this cursor.`);
590
+ else
591
+ lines.push(...value.threads.map(thread => `${thread.threadRef} · ${thread.channelRef}${thread.taskRef === undefined ? ' · taskless' : ` · ${taskStanding(thread)}`} — ${thread.subject}${thread.lastActivityAt === undefined ? '' : ` · last activity ${formatTeamTimestamp(thread.lastActivityAt)}`}`));
592
+ lines.push(`Thread cursor ${value.cursor}; hasMore=${value.hasMore ? 'true' : 'false'}${value.hasMore ? ' — older Thread anchors exist; page again with this cursor.' : ' — no older Threads remain.'}`);
593
+ if (!continuation) {
594
+ lines.push('', 'Members — current');
595
+ if (value.members.length === 0)
596
+ lines.push('No visible Members.');
597
+ else
598
+ lines.push(...value.members.map(m => `${m.memberId} · @${m.handle} (${m.kind}, ${m.presence})${m.description === '' ? '' : ` — ${m.description}`}`));
599
+ }
600
+ return [{ type: 'text', text: lines.join('\n') }];
601
+ },
412
602
  },
413
603
  async execute(args, exec) {
414
604
  const agent = exec.agent;
@@ -416,7 +606,7 @@ const teamView = defineTool({
416
606
  throw new Error('team_view requires an Agent session');
417
607
  const current = member(agent);
418
608
  const host = service(agent);
419
- const view = host.viewForAgent(agent, { workspaceId: current.workspaceId, ...(args.channelRef === undefined ? {} : { channelRef: args.channelRef }), ...(args.limit === undefined ? {} : { limit: args.limit }), ...(args.cursor === undefined ? {} : { cursor: args.cursor }), topLevelOnly: true, includeActivities: false });
609
+ const view = host.viewForAgent(agent, { workspaceId: current.workspaceId, ...(args.channelRef === undefined ? {} : { channelRef: args.channelRef }), ...(args.limit === undefined ? {} : { limit: args.limit }), ...(args.cursor === undefined ? {} : { cursor: args.cursor }), topLevelOnly: true, includeActivities: false, direction: 'before' });
420
610
  const visibleMemberIds = new Set(view.members.map(membership => membership.memberId));
421
611
  return {
422
612
  channels: view.channels.map(channel => ({ channelRef: channel.channelRef, name: channel.name })),
@@ -429,11 +619,13 @@ const teamView = defineTool({
429
619
  const thread = item.thread;
430
620
  const task = item.task;
431
621
  return { threadRef: thread.threadRef, channelRef: item.message.channelRef, revision: thread.revision, messageCount: item.messageCount,
622
+ subject: boundedSubject(item.message.body), lastActivityAt: item.lastActivityAt,
432
623
  ...(task === undefined ? {} : { taskRef: task.taskRef, status: task.status, ...(item.taskNumber === undefined ? {} : { taskNumber: item.taskNumber }) }) };
433
624
  }),
434
625
  tasks: view.tasks.map(task => ({ taskRef: task.taskRef, threadRef: task.threadRef, channelRef: task.channelRef,
435
626
  status: task.status, revision: view.threads.find(thread => thread.threadRef === task.threadRef)?.revision ?? 0 })),
436
627
  cursor: view.cursor, hasMore: view.hasMore,
628
+ ...(args.cursor === undefined ? {} : { page: 'threads' }),
437
629
  };
438
630
  },
439
631
  });