@wowyuarm/dsh-agent-team 0.1.7 → 0.1.8

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 (64) hide show
  1. package/package.json +1 -1
  2. package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +2 -0
  3. package/packages/agent-team/lib/index.js +58 -0
  4. package/packages/agent-team/lib/ledger.js +135 -12
  5. package/packages/agent-team/lib/member-context.js +1 -1
  6. package/packages/agent-team/lib/progress-nudge.js +320 -0
  7. package/packages/agent-team/lib/typert.host.js +27 -26
  8. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  9. package/packages/agent-team/lib/typert.remote-client.js +26 -25
  10. package/packages/agent-team/lib/types/index.d.ts +8 -0
  11. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  12. package/packages/agent-team/lib/types/ledger.d.ts +27 -2
  13. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  14. package/packages/agent-team/lib/types/progress-nudge.d.ts +136 -0
  15. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -0
  16. package/packages/agent-team/lib/types/types/entities.d.ts +18 -0
  17. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  18. package/packages/agent-team/lib/types/types/requests-results.d.ts +2 -0
  19. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  20. package/packages/agent-team/preset/team-member/agent.cordis.yml +2 -2
  21. package/packages/client-agent-team/lib/client.js +406 -512
  22. package/packages/client-agent-team/lib/client.js.map +1 -1
  23. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  24. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +3 -7
  25. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +3 -1
  26. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  27. package/packages/client-agent-team/lib/types/client/TeamComposer.js +37 -27
  28. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  29. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  30. package/packages/client-agent-team/lib/types/client/TeamConversation.js +2 -2
  31. package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.d.ts.map +1 -1
  32. package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.js +3 -2
  33. package/packages/client-agent-team/lib/types/client/TeamMessage.js +5 -0
  34. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +2 -2
  35. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +4 -6
  37. package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts +14 -0
  38. package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts.map +1 -0
  39. package/packages/client-agent-team/lib/types/client/TeamStateDot.js +15 -0
  40. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +1 -0
  41. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  42. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +41 -16
  43. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  44. package/packages/client-agent-team/lib/types/client/index.js +2 -67
  45. package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -2
  46. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  47. package/packages/client-agent-team/lib/types/client/locales.js +0 -2
  48. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +1 -2
  49. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/slots.d.ts +3 -1
  51. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  52. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +7 -1
  53. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/task-refs.js +25 -3
  55. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +11 -10
  56. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/team-formatters.js +14 -11
  58. package/packages/tool-agent-team/lib/index.js +9 -9
  59. package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts +0 -12
  60. package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts.map +0 -1
  61. package/packages/client-agent-team/lib/types/client/TeamMemberDock.js +0 -13
  62. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +0 -20
  63. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +0 -1
  64. package/packages/client-agent-team/lib/types/client/member-session-input.js +0 -110
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wowyuarm/dsh-agent-team",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "A persistent agent team for long-running collaboration in DeepSeek Harness",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,6 +7,8 @@ description: Create, install, revise, or retire this Member's private skills, in
7
7
 
8
8
  A skill preserves a reusable way of working. It is not a memory, a personal preference, or a record of one event.
9
9
 
10
+ Skills are private to this Member — no other human or agent can read this directory. When a Team message or your session output needs the method, restate the relevant content there; citing a skill by name does not transfer it to anyone.
11
+
10
12
  ## Decide
11
13
 
12
14
  Create or revise a skill when the method has been demonstrated in real work and will make later work clearer or more reliable. Keep one-off work, changing facts, and personal continuity in this Member's notes and memory index instead.
@@ -53,6 +53,7 @@ import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathA
53
53
  import { acceptedTaskCompactionMembers, AutoCompactionCoordinator, PRE_COMPACTION_NOTICE_SUMMARY, preCompactionNoticeText } from "./auto-compaction.js";
54
54
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
55
55
  import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, MemberRuntime } from "./member-runtime.js";
56
+ import { ProgressNudgeCoordinator } from "./progress-nudge.js";
56
57
  import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
57
58
  import { agentTeamDomainSpec } from "./spec.js";
58
59
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
@@ -189,6 +190,8 @@ let AgentTeam = (() => {
189
190
  domain = __runInitializers(this, _instanceExtraInitializers);
190
191
  ledger;
191
192
  handles = new Map();
193
+ /** Live Member per session id; drives the root session/event listener. */
194
+ memberBySessionId = new Map();
192
195
  /** Live selection refs let model edits take effect without disposing the Session. */
193
196
  modelSelections = new Map();
194
197
  /** Agent ids with a turn in flight; restarts must wait for the boundary. */
@@ -226,6 +229,22 @@ let AgentTeam = (() => {
226
229
  this.ctx.logger.warn(`agent-team: member '${this.memberLabel(memberId)}' reached ${consecutiveFailures}/${RECOVERY_MAX_CONSECUTIVE_ERRORS} consecutive recoverable failures; leaving it in error for the operator`);
227
230
  },
228
231
  });
232
+ /**
233
+ * Advisory progress-visibility nudges. Purely in-process: the ledger owns
234
+ * eligibility, this coordinator owns counting and notices, and neither ever
235
+ * writes Team facts. See docs/team-collaboration.md for the regimes.
236
+ */
237
+ progressNudge = new ProgressNudgeCoordinator({
238
+ agentForMember: memberId => this.handles.get(memberId)?.agent,
239
+ targetsForMember: memberId => this.requireLedger().progressNudgeTargets(memberId),
240
+ sessionLogForMember: (memberId, sessionId) => {
241
+ const handle = this.handles.get(memberId);
242
+ if (handle === undefined || handle.agent.session.id !== sessionId)
243
+ return undefined;
244
+ return { events: handle.agent.session.ownEvents() };
245
+ },
246
+ log: message => { this.ctx.logger.warn(message); },
247
+ });
229
248
  lifecycleTail = Promise.resolve();
230
249
  accepting = true;
231
250
  changeVersion = 0;
@@ -252,6 +271,10 @@ let AgentTeam = (() => {
252
271
  content: [{ type: 'text', text: preCompactionNoticeText() }],
253
272
  source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: PRE_COMPACTION_NOTICE_SUMMARY },
254
273
  });
274
+ // Persist-your-conclusions outranks a queued progress nudge.
275
+ const member = this.memberForAgent(agent);
276
+ if (member !== undefined)
277
+ this.progressNudge.revokePendingNotice(member.memberId);
255
278
  agent.steer(hint);
256
279
  },
257
280
  });
@@ -295,10 +318,28 @@ let AgentTeam = (() => {
295
318
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
296
319
  }
297
320
  });
321
+ // Progress nudges count every `tool/call` of each Member Session. The
322
+ // store's dispatch carrier is untagged, so a scope-tagged listener inside
323
+ // the Agent setup would receive nothing; one root listener that maps the
324
+ // session id back to its Member is the seam that works (same shape as the
325
+ // `agent/status` listener above). The first turn of a freshly published
326
+ // Member cannot race this: `handles.set()` precedes the same activation
327
+ // continuation that publishes the Agent, so by the time any tool call
328
+ // streams, the map lookup succeeds.
329
+ this.ctx.on('session/event', (session, event) => {
330
+ const memberId = this.memberBySessionId.get(session.id);
331
+ if (memberId === undefined)
332
+ return;
333
+ const handle = this.handles.get(memberId);
334
+ if (handle === undefined || handle.agent.session.id !== session.id)
335
+ return;
336
+ this.progressNudge.onSessionEvent(memberId, session.id, handle.agent, event);
337
+ });
298
338
  const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
299
339
  this.ctx.effect(() => async () => {
300
340
  this.accepting = false;
301
341
  this.recovery.dispose();
342
+ this.progressNudge.dispose();
302
343
  await this.autoCompaction.dispose();
303
344
  if (this.attachmentGcTimer !== undefined)
304
345
  clearInterval(this.attachmentGcTimer);
@@ -566,6 +607,10 @@ let AgentTeam = (() => {
566
607
  // Drop the old handle's transient state: pending recovery episodes and
567
608
  // error markers belong to the disposed agent, not to the Member.
568
609
  this.recovery.stopTracking(request.memberId);
610
+ // The fresh Session may re-earn one Claim suggestion per Thread; the
611
+ // old Session's one-shot records must not leak into it.
612
+ this.progressNudge.stopTracking(request.memberId);
613
+ this.memberBySessionId.delete(previousSessionId);
569
614
  await active.dispose();
570
615
  this.handles.delete(request.memberId);
571
616
  this.modelSelections.delete(request.memberId);
@@ -628,6 +673,9 @@ let AgentTeam = (() => {
628
673
  if (this.isInboxNotice(pending))
629
674
  handle.agent.inbox.remove(pending.id);
630
675
  }
676
+ // Recovery carries the concrete interrupted work; a queued progress nudge
677
+ // is stale next to it and must not survive as a second reminder.
678
+ this.progressNudge.revokePendingNotice(member.memberId);
631
679
  handle.agent.steer(hint);
632
680
  }
633
681
  /** Automatic-recovery wakeup; throwing tells the coordinator the target is gone. */
@@ -1090,6 +1138,7 @@ let AgentTeam = (() => {
1090
1138
  this.memberRuntime.mountMemberSkillProvider(member, created.agent.ctx, skillSelection);
1091
1139
  await workspace.attachSession(member.sessionId);
1092
1140
  this.handles.set(member.memberId, created);
1141
+ this.memberBySessionId.set(member.sessionId, member.memberId);
1093
1142
  this.modelSelections.set(member.memberId, selected);
1094
1143
  this.clearMemberFailure(member.memberId, 'activation');
1095
1144
  this.nameMemberSession(member, created.agent);
@@ -1233,6 +1282,10 @@ let AgentTeam = (() => {
1233
1282
  }
1234
1283
  const ledger = this.requireLedger();
1235
1284
  this.emitChanged(ledger.changeScopesOf(operation));
1285
+ // Nudge reconciliation precedes notifyMember: a committed reply both
1286
+ // resets the author's silence and cancels any of its queued nudge before
1287
+ // the Inbox notice (higher priority) is considered.
1288
+ this.progressNudge.onCommitted(operation);
1236
1289
  for (const memberId of ledger.affectedMembersOf(operation)) {
1237
1290
  const handle = this.handles.get(memberId);
1238
1291
  if (handle !== undefined)
@@ -1271,6 +1324,9 @@ let AgentTeam = (() => {
1271
1324
  const existingInboxHint = pending.find(message => this.isInboxNotice(message));
1272
1325
  if (existingInboxHint !== undefined)
1273
1326
  agent.inbox.remove(existingInboxHint.id);
1327
+ // A durable Inbox notice outranks a queued progress nudge; revoke it so
1328
+ // the model reads concrete unread work instead of a generic reminder.
1329
+ this.progressNudge.revokePendingNotice(member.memberId);
1274
1330
  const hint = createUserMessage({
1275
1331
  content: [{ type: 'text', text: this.notificationText(notifications, member.memberId) }],
1276
1332
  source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: INBOX_NOTICE_SUMMARY },
@@ -1376,12 +1432,14 @@ let AgentTeam = (() => {
1376
1432
  await handle.dispose();
1377
1433
  this.handles.delete(memberId);
1378
1434
  }
1435
+ this.memberBySessionId.delete(member.sessionId);
1379
1436
  this.modelSelections.delete(memberId);
1380
1437
  this.memberRuntime.forgetMember(memberId);
1381
1438
  this.clearMemberFailure(memberId, 'activation');
1382
1439
  }
1383
1440
  clearMemberRecoveryState(member) {
1384
1441
  this.recovery.stopTracking(member.memberId);
1442
+ this.progressNudge.stopTracking(member.memberId);
1385
1443
  this.clearMemberFailure(member.memberId, 'runtime');
1386
1444
  this.clearMemberNotificationState(member.memberId);
1387
1445
  }
@@ -19,6 +19,10 @@ function emptyProjection() {
19
19
  orderedFacts: [], factsByThread: new Map(), channelRefByThread: new Map(), mentionsByMessage: new Map(), messageCountByThread: new Map(),
20
20
  attentionByThread: new Map() };
21
21
  }
22
+ const EMPTY_PROGRESS_NUDGE_TARGETS = Object.freeze({
23
+ progress: Object.freeze([]),
24
+ claim: Object.freeze([]),
25
+ });
22
26
  /** Exhaustiveness guard for the closed operation union: adding a kind must update every dispatch. */
23
27
  function assertUnhandledKind(operation) {
24
28
  throw new Error(`agent-team ledger does not handle operation kind '${operation.kind}'`);
@@ -246,6 +250,80 @@ export class AgentTeamLedger {
246
250
  getMember(memberId) {
247
251
  return this.state.members.get(memberId);
248
252
  }
253
+ /**
254
+ * Current progress-nudge candidacy for one Member: threads whose Thread
255
+ * progress reminder applies (active Claim, or following a taskless Thread)
256
+ * and tasks where the Claim reminder applies (following a still-`todo` Task
257
+ * with no Claim history for this Member). A deep read-only projection — the
258
+ * caller never re-derives attention, claim, task-resolution, or channel
259
+ * archive state. Archived channels and resolved tasks produce no candidacy;
260
+ * results are ordered by Thread revision descending, then threadRef.
261
+ */
262
+ progressNudgeTargets(memberId) {
263
+ const member = this.state.members.get(memberId);
264
+ if (member === undefined || member.state !== 'enabled')
265
+ return EMPTY_PROGRESS_NUDGE_TARGETS;
266
+ const progress = [];
267
+ const claim = [];
268
+ const seenProgress = new Set();
269
+ for (const claimRecord of this.state.claims.values()) {
270
+ if (claimRecord.owner !== memberId || claimRecord.state !== 'active')
271
+ continue;
272
+ const task = this.state.tasks.get(claimRecord.taskRef);
273
+ const thread = this.state.threads.get(claimRecord.threadRef);
274
+ if (task === undefined || thread === undefined || task.resolution !== 'open')
275
+ continue;
276
+ if (this.state.channels.get(task.channelRef)?.state === 'archived')
277
+ continue;
278
+ if (seenProgress.has(claimRecord.threadRef))
279
+ continue;
280
+ seenProgress.add(claimRecord.threadRef);
281
+ progress.push({ reason: 'active-claim', threadRef: claimRecord.threadRef, taskRef: claimRecord.taskRef });
282
+ }
283
+ for (const [threadRef, followers] of this.state.attentionByThread) {
284
+ if (!followers.has(memberId))
285
+ continue;
286
+ const thread = this.state.threads.get(threadRef);
287
+ if (thread === undefined)
288
+ continue;
289
+ const channelRef = this.state.channelRefByThread.get(threadRef);
290
+ if (channelRef === undefined || this.state.channels.get(channelRef)?.state === 'archived')
291
+ continue;
292
+ if (thread.taskRef === undefined) {
293
+ if (!seenProgress.has(threadRef)) {
294
+ seenProgress.add(threadRef);
295
+ progress.push({ reason: 'taskless-follower', threadRef });
296
+ }
297
+ continue;
298
+ }
299
+ // Claim reminder: only still-`todo` tasks with zero lifetime claims by
300
+ // this Member; once claimed (any state) the Member moved into the
301
+ // progress-reminder regime.
302
+ if (this.hasLifetimeClaimOnTask(memberId, thread.taskRef))
303
+ continue;
304
+ const task = this.state.tasks.get(thread.taskRef);
305
+ if (task === undefined || task.resolution !== 'open' || this.deriveTaskStatus(task.taskRef, this.state.claims.values()) !== 'todo')
306
+ continue;
307
+ claim.push({ threadRef, taskRef: thread.taskRef });
308
+ }
309
+ const byThreadRevision = (left, right) => {
310
+ const leftRevision = this.state.threads.get(left.threadRef)?.revision ?? 0;
311
+ const rightRevision = this.state.threads.get(right.threadRef)?.revision ?? 0;
312
+ return rightRevision - leftRevision || (left.threadRef < right.threadRef ? -1 : left.threadRef > right.threadRef ? 1 : 0);
313
+ };
314
+ return Object.freeze({
315
+ progress: Object.freeze(progress.sort(byThreadRevision)),
316
+ claim: Object.freeze(claim.sort(byThreadRevision)),
317
+ });
318
+ }
319
+ /** Whether this Member has ever held a Claim on the Task, in any state. */
320
+ hasLifetimeClaimOnTask(memberId, taskRef) {
321
+ for (const claim of this.state.claims.values()) {
322
+ if (claim.owner === memberId && claim.taskRef === taskRef)
323
+ return true;
324
+ }
325
+ return false;
326
+ }
249
327
  listMembers() {
250
328
  return Object.freeze([...this.state.members.values()]);
251
329
  }
@@ -518,7 +596,7 @@ export class AgentTeamLedger {
518
596
  else {
519
597
  if (request.direction !== undefined)
520
598
  throw new Error(`${request.action} action does not accept direction`);
521
- const previous = request.claimRef === undefined ? undefined : this.state.claims.get(request.claimRef);
599
+ const previous = request.claimRef === undefined ? undefined : this.state.claims.get(this.requireRefKey(this.state.claims, request.claimRef, 'claim', 'Claim'));
522
600
  if (previous === undefined) {
523
601
  throw new Error(`unknown Claim '${request.claimRef ?? ''}'${request.claimRef === undefined ? '' : this.unknownRefHint(request.claimRef, 'claim', 'Claim')}`);
524
602
  }
@@ -897,7 +975,7 @@ export class AgentTeamLedger {
897
975
  memberId: next.memberId, action: 'follow' }));
898
976
  }
899
977
  }
900
- return Object.freeze({ items: Object.freeze(observations.slice(-limit)) });
978
+ return Object.freeze({ items: Object.freeze(observations.slice(-limit)), followers: Object.freeze([...current.keys()]) });
901
979
  }
902
980
  /** Every operation carrying an inbox delta drives the Inbox projection; the payload shape decides, not a per-kind list. */
903
981
  attentionDelta(operation) {
@@ -946,7 +1024,8 @@ export class AgentTeamLedger {
946
1024
  const numbers = this.taskNumbers(workspaceId);
947
1025
  const resolved = [];
948
1026
  for (const taskRef of taskRefs) {
949
- const task = this.state.tasks.get(taskRef);
1027
+ const key = this.uniqueRefKey(this.state.tasks, taskRef, 'task');
1028
+ const task = key === undefined ? undefined : this.state.tasks.get(key);
950
1029
  const channel = task === undefined ? undefined : this.state.channels.get(task.channelRef);
951
1030
  // Archived Channels do not exist on Team API surfaces: their Tasks stop
952
1031
  // resolving, so live message bodies render the refs as plain
@@ -2433,13 +2512,55 @@ export class AgentTeamLedger {
2433
2512
  throw new Error('agent-team operation lacks enabled Member authority');
2434
2513
  return member;
2435
2514
  }
2436
- /** Agents sometimes strip the branded prefix when echoing refs; point at the fix instead of a bare lookup failure. */
2515
+ /** Shortest accepted UUID abbreviation after the branded prefix. */
2516
+ static MIN_REF_UUID_PREFIX = 6;
2517
+ isRefAbbreviationTail(tail) {
2518
+ return tail.length >= AgentTeamLedger.MIN_REF_UUID_PREFIX && /^[0-9a-f]+$/i.test(tail);
2519
+ }
2520
+ /**
2521
+ * Full map key for one branded ref, tolerating a unique UUID abbreviation.
2522
+ * Exact refs win; an abbreviated ref (prefix plus at least 6 hex chars,
2523
+ * hyphens ignored) resolves only when it matches exactly one key. Returns
2524
+ * undefined for unknown or ambiguous refs so lenient callers can degrade
2525
+ * without throwing.
2526
+ */
2527
+ uniqueRefKey(map, ref, prefix) {
2528
+ if (map.has(ref))
2529
+ return ref;
2530
+ const tail = ref.startsWith(`${prefix}:`) ? ref.slice(prefix.length + 1).replaceAll('-', '') : undefined;
2531
+ if (tail === undefined || !this.isRefAbbreviationTail(tail))
2532
+ return undefined;
2533
+ const lower = tail.toLowerCase();
2534
+ const candidates = [...map.keys()].filter(key => key.slice(prefix.length + 1).replaceAll('-', '').startsWith(lower));
2535
+ return candidates.length === 1 ? candidates[0] : undefined;
2536
+ }
2537
+ /** uniqueRefKey with the established unknown/ambiguous error contract and hints. */
2538
+ requireRefKey(map, ref, prefix, label) {
2539
+ const key = this.uniqueRefKey(map, ref, prefix);
2540
+ if (key !== undefined)
2541
+ return key;
2542
+ const tail = ref.startsWith(`${prefix}:`) ? ref.slice(prefix.length + 1).replaceAll('-', '') : undefined;
2543
+ if (tail !== undefined && this.isRefAbbreviationTail(tail)) {
2544
+ const lower = tail.toLowerCase();
2545
+ const candidates = [...map.keys()].filter(candidate => candidate.slice(prefix.length + 1).replaceAll('-', '').startsWith(lower));
2546
+ if (candidates.length > 1) {
2547
+ throw new Error(`ambiguous ${label} ref '${ref}' matches ${candidates.map(candidate => `'${candidate}'`).join(', ')}; reuse a longer prefix or the full ref exactly as returned by Team tools`);
2548
+ }
2549
+ }
2550
+ throw new Error(`unknown ${label} ref '${ref}'${this.unknownRefHint(ref, prefix, label)}`);
2551
+ }
2552
+ /** Agents strip the branded prefix or abbreviate UUIDs when echoing refs; point at the fix instead of a bare lookup failure. */
2437
2553
  unknownRefHint(ref, prefix, label) {
2438
- return ref.startsWith(`${prefix}:`) ? ''
2439
- : ` A ${label} ref must start with '${prefix}:'; reuse the full ref exactly as returned by Team tools ('${prefix}:${ref}').`;
2554
+ if (!ref.startsWith(`${prefix}:`)) {
2555
+ return ` A ${label} ref must start with '${prefix}:'; reuse the full ref exactly as returned by Team tools ('${prefix}:${ref}').`;
2556
+ }
2557
+ const tail = ref.slice(prefix.length + 1).replaceAll('-', '');
2558
+ return this.isRefAbbreviationTail(tail)
2559
+ ? ` No ${label} matches this UUID prefix; reuse the full ref exactly as returned by Team tools.`
2560
+ : ` A ${label} ref needs at least 6 hex characters after '${prefix}:', or the full ref exactly as returned by Team tools.`;
2440
2561
  }
2441
2562
  requireTask(workspaceId, taskRef) {
2442
- const task = this.state.tasks.get(taskRef);
2563
+ const task = this.state.tasks.get(this.requireRefKey(this.state.tasks, taskRef, 'task', 'Task'));
2443
2564
  if (task === undefined)
2444
2565
  throw new Error(`unknown Task ref '${taskRef}'${this.unknownRefHint(taskRef, 'task', 'Task')}`);
2445
2566
  if (this.state.channels.get(task.channelRef)?.workspaceId !== workspaceId)
@@ -2447,7 +2568,7 @@ export class AgentTeamLedger {
2447
2568
  return task;
2448
2569
  }
2449
2570
  requireThread(threadRef) {
2450
- const thread = this.state.threads.get(threadRef);
2571
+ const thread = this.state.threads.get(this.requireRefKey(this.state.threads, threadRef, 'thread', 'Thread'));
2451
2572
  if (thread === undefined)
2452
2573
  throw new Error(`unknown Thread ref '${threadRef}'${this.unknownRefHint(threadRef, 'thread', 'Thread')}`);
2453
2574
  return thread;
@@ -2470,13 +2591,15 @@ export class AgentTeamLedger {
2470
2591
  let thread;
2471
2592
  let task;
2472
2593
  if (request.threadRef !== undefined) {
2473
- thread = projection.threads.get(request.threadRef);
2594
+ const threadKey = this.requireRefKey(projection.threads, request.threadRef, 'thread', 'Thread');
2595
+ thread = projection.threads.get(threadKey);
2474
2596
  if (thread === undefined)
2475
2597
  throw new Error(`unknown Thread ref '${request.threadRef}'${this.unknownRefHint(request.threadRef, 'thread', 'Thread')}`);
2476
2598
  task = thread.taskRef === undefined ? undefined : projection.tasks.get(thread.taskRef);
2477
2599
  }
2478
2600
  if (request.taskRef !== undefined) {
2479
- const byTask = projection.tasks.get(request.taskRef);
2601
+ const taskKey = this.requireRefKey(projection.tasks, request.taskRef, 'task', 'Task');
2602
+ const byTask = projection.tasks.get(taskKey);
2480
2603
  if (byTask === undefined)
2481
2604
  throw new Error(`unknown Task ref '${request.taskRef}'${this.unknownRefHint(request.taskRef, 'task', 'Task')}`);
2482
2605
  if (task !== undefined && task.taskRef !== byTask.taskRef)
@@ -2507,7 +2630,7 @@ export class AgentTeamLedger {
2507
2630
  return { ...(task === undefined ? {} : { task }), thread, channelRef };
2508
2631
  }
2509
2632
  requireChannel(workspaceId, channelRef) {
2510
- const channel = this.state.channels.get(channelRef);
2633
+ const channel = this.state.channels.get(this.requireRefKey(this.state.channels, channelRef, 'channel', 'Channel'));
2511
2634
  if (channel === undefined)
2512
2635
  throw new Error(`unknown Channel ref '${channelRef}'${this.unknownRefHint(channelRef, 'channel', 'Channel')}`);
2513
2636
  if (channel.workspaceId !== workspaceId)
@@ -2528,7 +2651,7 @@ export class AgentTeamLedger {
2528
2651
  }
2529
2652
  }
2530
2653
  requireMember(memberId) {
2531
- const member = this.state.members.get(memberId);
2654
+ const member = this.state.members.get(this.requireRefKey(this.state.members, memberId, 'member', 'Agent Member'));
2532
2655
  if (member === undefined)
2533
2656
  throw new Error(`unknown Agent Member '${memberId}'`);
2534
2657
  return member;
@@ -63,7 +63,7 @@ export function renderMemberMemory(raw, privateMemoryPath = '<private-memory-pat
63
63
  const warning = overBudget
64
64
  ? '\n\n[Maintenance warning: memory.md exceeds the 8 KiB context budget. Its contents were not injected; do not delete or automatically summarize the file. Maintain a smaller index explicitly.]'
65
65
  : '';
66
- return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
66
+ return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Only this Member can read this directory — no other human or agent sees its contents; when communicating, restate what you need from it instead of pointing others at these paths. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
67
67
  }
68
68
  function renderUnavailableMemory(privateMemoryPath, reason) {
69
69
  return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;