@wowyuarm/dsh-agent-team 0.1.11 → 0.1.13

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 (114) hide show
  1. package/README.md +6 -4
  2. package/README.zh.md +6 -4
  3. package/cordis.patch.yml +7 -3
  4. package/package.json +16 -3
  5. package/packages/agent-team/README.md +4 -4
  6. package/packages/agent-team/README.zh.md +4 -4
  7. package/packages/agent-team/lib/context-projection.js +5 -1
  8. package/packages/agent-team/lib/index.js +162 -113
  9. package/packages/agent-team/lib/ledger.js +496 -169
  10. package/packages/agent-team/lib/member-context.js +18 -1
  11. package/packages/agent-team/lib/member-runtime.js +48 -9
  12. package/packages/agent-team/lib/mentions.js +80 -0
  13. package/packages/agent-team/lib/spec.js +20 -0
  14. package/packages/agent-team/lib/typert.host.js +241 -46
  15. package/packages/agent-team/lib/typert.remote-client.d.ts +7 -3
  16. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  17. package/packages/agent-team/lib/typert.remote-client.js +178 -28
  18. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  19. package/packages/agent-team/lib/types/index.d.ts +33 -14
  20. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  21. package/packages/agent-team/lib/types/ledger.d.ts +120 -18
  22. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/member-context.d.ts +6 -0
  24. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/member-runtime.d.ts +2 -1
  26. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/mentions.d.ts +47 -0
  28. package/packages/agent-team/lib/types/mentions.d.ts.map +1 -0
  29. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/types/entities.d.ts +3 -20
  31. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/types/operations.d.ts +37 -1
  33. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  34. package/packages/agent-team/lib/types/types/requests-results.d.ts +107 -25
  35. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/vendor/storage-sqlite/index.d.ts +87 -0
  37. package/packages/agent-team/lib/types/vendor/storage-sqlite/index.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/vendor/storage-sqlite/schema.d.ts +59 -0
  39. package/packages/agent-team/lib/types/vendor/storage-sqlite/schema.d.ts.map +1 -0
  40. package/packages/agent-team/lib/types/vendor/storage-sqlite/unit.d.ts +57 -0
  41. package/packages/agent-team/lib/types/vendor/storage-sqlite/unit.d.ts.map +1 -0
  42. package/packages/agent-team/lib/vendor/storage-sqlite/index.js +139 -0
  43. package/packages/agent-team/lib/vendor/storage-sqlite/schema.js +120 -0
  44. package/packages/agent-team/lib/vendor/storage-sqlite/unit.js +132 -0
  45. package/packages/agent-team/preset/team-member/agent.cordis.yml +48 -8
  46. package/packages/client-agent-team/README.md +1 -1
  47. package/packages/client-agent-team/README.zh.md +1 -1
  48. package/packages/client-agent-team/lib/client.js +2123 -1406
  49. package/packages/client-agent-team/lib/client.js.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamAgentImport.d.ts +12 -0
  51. package/packages/client-agent-team/lib/types/client/TeamAgentImport.d.ts.map +1 -0
  52. package/packages/client-agent-team/lib/types/client/TeamAgentImport.js +69 -0
  53. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +3 -1
  54. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +31 -19
  56. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +18 -0
  57. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -0
  58. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +22 -0
  59. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +3 -1
  60. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  61. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +111 -24
  62. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  63. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +10 -3
  64. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  65. package/packages/client-agent-team/lib/types/client/TeamComposer.js +11 -17
  66. package/packages/client-agent-team/lib/types/client/TeamConversation.js +1 -1
  67. package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts +20 -0
  68. package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts.map +1 -0
  69. package/packages/client-agent-team/lib/types/client/TeamCountBadge.js +25 -0
  70. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +9 -5
  71. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +92 -23
  73. package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts +43 -0
  74. package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts.map +1 -0
  75. package/packages/client-agent-team/lib/types/client/TeamMemberRow.js +27 -0
  76. package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts +2 -2
  77. package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts.map +1 -1
  78. package/packages/client-agent-team/lib/types/client/TeamMembersAction.js +2 -2
  79. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +1 -1
  80. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  82. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +21 -31
  83. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  84. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  85. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +29 -11
  86. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  87. package/packages/client-agent-team/lib/types/client/index.js +11 -5
  88. package/packages/client-agent-team/lib/types/client/locales.d.ts +54 -16
  89. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  90. package/packages/client-agent-team/lib/types/client/locales.js +54 -16
  91. package/packages/client-agent-team/lib/types/client/slots.d.ts +3 -2
  92. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  93. package/packages/client-agent-team/lib/types/client/team-changes.d.ts +15 -25
  94. package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
  95. package/packages/client-agent-team/lib/types/client/team-changes.js +82 -80
  96. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +45 -10
  97. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  98. package/packages/client-agent-team/lib/types/client/team-formatters.js +71 -30
  99. package/packages/tool-agent-team/README.md +3 -3
  100. package/packages/tool-agent-team/README.zh.md +3 -3
  101. package/packages/tool-agent-team/lib/host-access.js +19 -0
  102. package/packages/tool-agent-team/lib/index.js +71 -49
  103. package/packages/tool-agent-team/lib/types/host-access.d.ts +10 -0
  104. package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -1
  105. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  106. package/packages/agent-team/lib/progress-nudge.js +0 -328
  107. package/packages/agent-team/lib/types/progress-nudge.d.ts +0 -136
  108. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +0 -1
  109. package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts +0 -3
  110. package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts.map +0 -1
  111. package/packages/client-agent-team/lib/types/client/TeamSettings.js +0 -5
  112. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +0 -41
  113. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +0 -1
  114. package/packages/client-agent-team/lib/types/client/scope-coverage.js +0 -64
@@ -1,8 +1,6 @@
1
- import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
2
- import type { AgentTeamChangeScope, AgentTeamChangesRequest, AgentTeamChangesResult } from '@wowyuarm/dsh-agent-team/types';
3
- /** One poll observes one scope, or every Team change when the scope is omitted. */
1
+ import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client';
2
+ import type { AgentTeamChangeScope } from '@wowyuarm/dsh-agent-team/types';
4
3
  export type TeamChangeScope = AgentTeamChangeScope | undefined;
5
- /** One invalidation delivered to every surface subscribed to one scope. */
6
4
  export type TeamChangeUpdate = {
7
5
  readonly type: 'changed';
8
6
  readonly version: number;
@@ -11,30 +9,23 @@ export type TeamChangeUpdate = {
11
9
  readonly message: string;
12
10
  };
13
11
  export type TeamChangeListener = (update: TeamChangeUpdate) => void;
14
- type ChangesFn = (request: AgentTeamChangesRequest, signal?: AbortSignal) => Promise<RemoteResult<AgentTeamChangesResult>>;
15
- /**
16
- * One long-poll per change scope, shared by every listening surface: panels
17
- * and pages never open parallel `changes` requests for the same scope, and
18
- * the poll is aborted as soon as the last subscriber leaves.
19
- *
20
- * Each poll keeps its own cursor, and a cursor only means something inside the
21
- * scope it was issued for: the Host answers a presence scope from a
22
- * process-local wake epoch and every other scope from a durable ledger
23
- * position.
24
- */
12
+ /** One logical stream per scope per page; Harness owns the shared transport and recovery. */
25
13
  export declare class TeamChangeStream {
26
- private readonly changes;
27
- private readonly polls;
14
+ private readonly remote;
15
+ private readonly subscriptions;
16
+ constructor(remote: Pick<ClientRemote, '$stream' | 'agentTeam'>);
17
+ subscribe(scope: TeamChangeScope, listener: TeamChangeListener): () => void;
28
18
  /**
29
- * The last version each scope key answered with, kept after that scope's poll
30
- * is torn down: a commit landing while no poll exists is otherwise folded into
31
- * the replacement poll's opening sample, which reports nothing.
19
+ * Reopen every scope whose stream already ended for good. The Harness resumes a
20
+ * live generation across reconnects, but a terminated one is gone for good: a new
21
+ * Host generation is the moment the scope was waiting for can come back, and the
22
+ * listeners keep their seats, so only the stream is replaced.
32
23
  */
33
- private readonly observed;
34
- constructor(changes: ChangesFn);
35
- subscribe(scope: TeamChangeScope, listener: TeamChangeListener): () => void;
36
- private run;
24
+ recover(): void;
25
+ private open;
26
+ dispose(): Promise<void>;
37
27
  private fail;
28
+ private run;
38
29
  }
39
30
  /**
40
31
  * The Host's `changes` stream never wakes on a Thread read — a read advances
@@ -49,5 +40,4 @@ export declare class TeamReadStream {
49
40
  bump(): void;
50
41
  subscribe(listener: () => void): () => void;
51
42
  }
52
- export {};
53
43
  //# sourceMappingURL=team-changes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"team-changes.d.ts","sourceRoot":"","sources":["../../../src/client/team-changes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AAE3H,mFAAmF;AACnF,MAAM,MAAM,eAAe,GAAG,oBAAoB,GAAG,SAAS,CAAA;AAE9D,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEzD,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;AAEnE,KAAK,SAAS,GAAG,CAAC,OAAO,EAAE,uBAAuB,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAC,CAAA;AAiB1H;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IASf,OAAO,CAAC,QAAQ,CAAC,OAAO;IARpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;gBAExB,OAAO,EAAE,SAAS;IAE/C,SAAS,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;YAmB7D,GAAG;IA2CjB,OAAO,CAAC,IAAI;CAKb;AAED;;;;;;GAMG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD,IAAI,IAAI,IAAI;IAKZ,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;CAI5C"}
1
+ {"version":3,"file":"team-changes.d.ts","sourceRoot":"","sources":["../../../src/client/team-changes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAgB,MAAM,qCAAqC,CAAA;AAErF,OAAO,KAAK,EAAE,oBAAoB,EAA0B,MAAM,gCAAgC,CAAA;AAElG,MAAM,MAAM,eAAe,GAAG,oBAAoB,GAAG,SAAS,CAAA;AAE9D,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEzD,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;AAiBnE,6FAA6F;AAC7F,qBAAa,gBAAgB;IAGf,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAuC;gBAExC,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,WAAW,CAAC;IAEhF,SAAS,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAmB3E;;;;;OAKG;IACH,OAAO,IAAI,IAAI;IAYf,OAAO,CAAC,IAAI;IAiBN,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM9B,OAAO,CAAC,IAAI;YAOE,GAAG;CAclB;AAED;;;;;;GAMG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD,IAAI,IAAI,IAAI;IAKZ,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;CAI5C"}
@@ -1,3 +1,4 @@
1
+ import { RemoteStreamCarrierError } from '@deepseek-ai/dsh-api-gateway/client';
1
2
  function scopeKey(scope) {
2
3
  return scope === undefined ? 'all'
3
4
  : scope.kind === 'workspace' ? `workspace:${scope.workspaceId}`
@@ -5,99 +6,100 @@ function scopeKey(scope) {
5
6
  : scope.kind === 'presence' ? `presence:${scope.workspaceId}`
6
7
  : `thread:${scope.threadRef}`;
7
8
  }
8
- /**
9
- * One long-poll per change scope, shared by every listening surface: panels
10
- * and pages never open parallel `changes` requests for the same scope, and
11
- * the poll is aborted as soon as the last subscriber leaves.
12
- *
13
- * Each poll keeps its own cursor, and a cursor only means something inside the
14
- * scope it was issued for: the Host answers a presence scope from a
15
- * process-local wake epoch and every other scope from a durable ledger
16
- * position.
17
- */
9
+ /** One logical stream per scope per page; Harness owns the shared transport and recovery. */
18
10
  export class TeamChangeStream {
19
- changes;
20
- polls = new Map();
21
- /**
22
- * The last version each scope key answered with, kept after that scope's poll
23
- * is torn down: a commit landing while no poll exists is otherwise folded into
24
- * the replacement poll's opening sample, which reports nothing.
25
- */
26
- observed = new Map();
27
- constructor(changes) {
28
- this.changes = changes;
11
+ remote;
12
+ subscriptions = new Map();
13
+ constructor(remote) {
14
+ this.remote = remote;
29
15
  }
30
16
  subscribe(scope, listener) {
31
17
  const key = scopeKey(scope);
32
- let poll = this.polls.get(key);
33
- if (poll === undefined) {
34
- poll = { controller: new AbortController(), listeners: new Set(), resumeFrom: this.observed.get(key) };
35
- this.polls.set(key, poll);
36
- void this.run(key, scope, poll);
18
+ let subscription = this.subscriptions.get(key);
19
+ if (subscription === undefined) {
20
+ subscription = this.open(scope, new Set([listener]));
21
+ this.subscriptions.set(key, subscription);
22
+ void this.run(key, subscription);
23
+ }
24
+ else {
25
+ subscription.listeners.add(listener);
26
+ if (subscription.failure !== undefined)
27
+ listener({ type: 'failed', message: subscription.failure });
37
28
  }
38
- poll.listeners.add(listener);
29
+ const owned = subscription;
39
30
  return () => {
40
- const current = this.polls.get(key);
41
- if (current === undefined || !current.listeners.delete(listener))
31
+ if (!owned.listeners.delete(listener) || owned.listeners.size !== 0)
42
32
  return;
43
- if (current.listeners.size === 0) {
44
- this.polls.delete(key);
45
- current.controller.abort();
46
- }
33
+ if (this.subscriptions.get(key) === owned)
34
+ this.subscriptions.delete(key);
35
+ void owned.stream.dispose();
47
36
  };
48
37
  }
49
- async run(key, scope, poll) {
50
- const { signal } = poll.controller;
51
- const request = (afterVersion) => ({ afterVersion, ...(scope === undefined ? {} : { scope }) });
52
- let version = poll.resumeFrom;
53
- if (version === undefined) {
54
- // Nothing has been observed for this scope yet, so there is no cursor to
55
- // resume from: sample the current version silently, because the
56
- // subscribers just fetched their initial projection and an immediate wake
57
- // would double-fetch. This leaves one named gap a commit landing between
58
- // that initial fetch being processed and this sample being answered is
59
- // missed until the next commit in the scope — which resuming from a known
60
- // version closes for every later subscription.
61
- const probe = await this.changes(request(0), signal);
62
- if (signal.aborted)
63
- return;
64
- if (!probe.ok) {
65
- this.fail(key, poll, probe.error.message);
66
- return;
67
- }
68
- version = probe.value.version;
69
- this.observed.set(key, version);
70
- }
71
- while (!signal.aborted) {
72
- const result = await this.changes(request(version), signal);
73
- if (signal.aborted)
74
- return;
75
- if (!result.ok) {
76
- this.fail(key, poll, result.error.message);
77
- return;
78
- }
79
- this.observed.set(key, result.value.version);
80
- // Any difference is a resolution in this scope's own cursor domain —
81
- // normally growth, but a cursor left over from another domain (the two
82
- // scopes count different things) or from an earlier Host lifetime can sit
83
- // ahead of the domain value. Re-anchoring on difference instead of only
84
- // on growth keeps such a subscription observable rather than silently
85
- // parked, and the Host never answers a request whose cursor already
86
- // equals the domain value, so this cannot spin.
87
- if (result.value.version !== version) {
88
- version = result.value.version;
89
- for (const listener of poll.listeners)
90
- listener({ type: 'changed', version });
91
- }
38
+ /**
39
+ * Reopen every scope whose stream already ended for good. The Harness resumes a
40
+ * live generation across reconnects, but a terminated one is gone for good: a new
41
+ * Host generation is the moment the scope was waiting for can come back, and the
42
+ * listeners keep their seats, so only the stream is replaced.
43
+ */
44
+ recover() {
45
+ // Replacing an existing key's value is safe during Map iteration: the entry keeps
46
+ // its place, and a scope that never failed is left untouched.
47
+ for (const [key, subscription] of this.subscriptions) {
48
+ if (subscription.failure === undefined)
49
+ continue;
50
+ const replacement = this.open(subscription.scope, subscription.listeners);
51
+ this.subscriptions.set(key, replacement);
52
+ void subscription.stream.dispose();
53
+ void this.run(key, replacement);
92
54
  }
93
55
  }
94
- fail(key, poll, message) {
95
- // A dead poll must not stay registered: the next subscriber restarts it.
96
- if (this.polls.get(key) === poll)
97
- this.polls.delete(key);
98
- for (const listener of poll.listeners)
56
+ open(scope, listeners) {
57
+ const key = scopeKey(scope);
58
+ const stream = this.remote.$stream({
59
+ name: `Team changes ${key}`,
60
+ open: signal => this.remote.agentTeam.changes(scope === undefined ? {} : { scope }, signal),
61
+ // A normal end after the baseline is an outage, not a verdict: the Harness
62
+ // retries a carrier loss and reports it through `carrierFailed`, while every
63
+ // other error stays terminal. Only an end before the opening baseline is a
64
+ // protocol violation — this stream never yields nothing before it ends.
65
+ ended: accepted => accepted
66
+ ? new RemoteStreamCarrierError('Team change subscription ended without a terminal result')
67
+ : new Error('Team change subscription ended before its opening baseline'),
68
+ carrierFailed: error => this.fail(key, error.message),
69
+ });
70
+ return { scope, stream, listeners, failure: undefined };
71
+ }
72
+ async dispose() {
73
+ const subscriptions = [...this.subscriptions.values()];
74
+ this.subscriptions.clear();
75
+ await Promise.all(subscriptions.map(subscription => subscription.stream.dispose()));
76
+ }
77
+ fail(key, message) {
78
+ const subscription = this.subscriptions.get(key);
79
+ if (subscription === undefined || subscription.failure !== undefined)
80
+ return;
81
+ subscription.failure = message;
82
+ for (const listener of subscription.listeners)
99
83
  listener({ type: 'failed', message });
100
84
  }
85
+ async run(key, subscription) {
86
+ try {
87
+ for await (const item of subscription.stream) {
88
+ if (this.subscriptions.get(key) !== subscription)
89
+ return;
90
+ item.accept();
91
+ subscription.failure = undefined;
92
+ // Every opening baseline invalidates too: this closes the initial-read
93
+ // race and recovers failed reads even when nothing changed while offline.
94
+ for (const listener of subscription.listeners)
95
+ listener({ type: 'changed', version: item.value.version });
96
+ }
97
+ }
98
+ catch (error) {
99
+ if (this.subscriptions.get(key) === subscription)
100
+ this.fail(key, error instanceof Error ? error.message : String(error));
101
+ }
102
+ }
101
103
  }
102
104
  /**
103
105
  * The Host's `changes` stream never wakes on a Thread read — a read advances
@@ -1,4 +1,4 @@
1
- import type { AgentTeamActivity, AgentTeamClaim, AgentTeamMemberId, AgentTeamTask, AgentTeamTaskRef } from '@wowyuarm/dsh-agent-team/types';
1
+ import type { AgentTeamActivity, AgentTeamClaim, AgentTeamClientMemberStatus, AgentTeamMemberId, AgentTeamTask, AgentTeamTaskRef } from '@wowyuarm/dsh-agent-team/types';
2
2
  import type { TeamConversationProps } from './slots.ts';
3
3
  import type { TeamStateDotState } from './TeamStateDot.tsx';
4
4
  export declare function formatTaskStatus(status: AgentTeamTask['status'], t: TeamConversationProps['t']): string;
@@ -42,18 +42,50 @@ export interface MentionSegment {
42
42
  readonly name?: string;
43
43
  }
44
44
  /**
45
- * Locate one Message's structured mention names inside its literal body. A
46
- * name matches case-insensitively with an optional leading '@' on Unicode word
47
- * boundaries, and longer names win at the same position. Mention segments
48
- * render the canonical `@Handle`; names absent from the body come back
49
- * unmatched so the consumer can append them as a fallback chip row.
45
+ * Locate one Message's delivered mention names inside its literal body. Matching
46
+ * is the shared Host delivery scan — an authored `@`, case-insensitive on Unicode
47
+ * word boundaries, longest handle first, code quoted rather than called so a chip
48
+ * never lands where delivery would not reach. Mention segments render the canonical
49
+ * `@Handle`; names absent from the body come back unmatched so the consumer can
50
+ * append them as a fallback chip row.
50
51
  */
51
52
  export declare function splitMentionNames(text: string, names: readonly string[]): {
52
53
  segments: MentionSegment[];
53
54
  unmatched: readonly string[];
54
55
  };
56
+ /**
57
+ * Whether one draft spells a handle as an authored `@mention`: the shared
58
+ * delivery scan, so the composer's recipient prune and will-notify preview agree
59
+ * with the Host on what the draft actually calls.
60
+ */
61
+ export declare function containsMention(body: string, handle: string): boolean;
62
+ /** Whether one draft carries the `@all` marker the mention menu expands. */
63
+ export declare function containsAllMention(body: string): boolean;
64
+ /**
65
+ * Every Member the mention menu's `@all` row stands for: the roster this
66
+ * composer was handed minus Members who cannot take a Message right now — the
67
+ * same filter the per-handle rows apply, so the expansion and the menu agree.
68
+ */
69
+ export declare function allMentionMembers(members: readonly AgentTeamClientMemberStatus[]): readonly AgentTeamClientMemberStatus[];
70
+ /**
71
+ * Member ids one draft asks to notify by text alone: the Client's preview of
72
+ * the Host's own body mention resolution, so a hand-typed `@Handle` reports
73
+ * exactly like a pick from the mention menu. `@all` stands for the menu's
74
+ * expansion; a written handle counts whenever its Member can still take a
75
+ * Message — state decides, not presence, because an offline Member is notified
76
+ * and reads it later.
77
+ *
78
+ * The result is preview-only. Only picked recipients travel as explicit
79
+ * recipients, where a name the Channel cannot reach would be a rejected target
80
+ * rather than the prose the Host reads.
81
+ */
82
+ export declare function mentionedMemberIds(body: string, members: readonly AgentTeamClientMemberStatus[]): readonly AgentTeamMemberId[];
55
83
  /** Canonical chip handles for one Message's structured mention refs. */
56
84
  export declare function mentionNamesOf(mentions: readonly AgentTeamMemberId[], handles: ReadonlyMap<AgentTeamMemberId, string>): string[];
85
+ /** Accessible label for one "who is on this work" stack: its owners' handles, comma-separated. */
86
+ export declare function claimersLabel(owners: ReadonlyArray<{
87
+ readonly name: string;
88
+ }>, t: TeamConversationProps['t']): string;
57
89
  /**
58
90
  * Whether an Agent body survives literal rendering unchanged: no fences,
59
91
  * inline code, emphasis markers, links, images, tables, or block constructs.
@@ -70,10 +102,13 @@ export declare function formatAbsoluteTime(occurredAt: string): string;
70
102
  */
71
103
  export declare function formatMessageTime(occurredAt: string, now?: Date): string;
72
104
  /**
73
- * Queue label for one Inbox row's newest mention. The mention queue scans by
74
- * recency, so the two days a reader reasons about by name are named ("今天" /
75
- * "昨天") while everything older keeps the Message date form, so the row and
76
- * the Thread it opens agree about the same instant.
105
+ * Recency label for one Inbox row's newest fact, shared with the Channel feed's
106
+ * entry line so the two agree about the same instant. Today is a bare clock time
107
+ * the reader is in today, and 「今天」 printed down every row spends the label's
108
+ * first word on the one segment that never varies, while `HH:mm` alone still
109
+ * reads as a clock because it is exactly one. The first day that is not today is
110
+ * the fact the reader has to be told, so it keeps its word; everything older
111
+ * keeps the Message date form, so the row and the Thread it opens agree.
77
112
  */
78
113
  export declare function formatInboxTime(occurredAt: string, t: TeamConversationProps['t'], now?: Date): string;
79
114
  export declare function formatActivity(activity: AgentTeamActivity, options: {
@@ -1 +1 @@
1
- {"version":3,"file":"team-formatters.d.ts","sourceRoot":"","sources":["../../../src/client/team-formatters.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"team-formatters.d.ts","sourceRoot":"","sources":["../../../src/client/team-formatters.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { hasAllMarker, resolveBodyMentions, scanBodyHandles } from '@wowyuarm/dsh-agent-team/mentions';
1
2
  export function formatTaskStatus(status, t) {
2
3
  return t({
3
4
  todo: 'taskStatusTodo',
@@ -81,42 +82,74 @@ export function isSingleBrandedRef(text) {
81
82
  const matches = [...trimmed.matchAll(BRANDED_REF_PATTERN)];
82
83
  return matches.length === 1 && matches[0][0] === trimmed;
83
84
  }
84
- function escapeRegExp(value) {
85
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
86
- }
87
85
  /**
88
- * Locate one Message's structured mention names inside its literal body. A
89
- * name matches case-insensitively with an optional leading '@' on Unicode word
90
- * boundaries, and longer names win at the same position. Mention segments
91
- * render the canonical `@Handle`; names absent from the body come back
92
- * unmatched so the consumer can append them as a fallback chip row.
86
+ * Locate one Message's delivered mention names inside its literal body. Matching
87
+ * is the shared Host delivery scan — an authored `@`, case-insensitive on Unicode
88
+ * word boundaries, longest handle first, code quoted rather than called so a chip
89
+ * never lands where delivery would not reach. Mention segments render the canonical
90
+ * `@Handle`; names absent from the body come back unmatched so the consumer can
91
+ * append them as a fallback chip row.
93
92
  */
94
93
  export function splitMentionNames(text, names) {
95
94
  if (names.length === 0)
96
95
  return { segments: [{ text, mention: false }], unmatched: [] };
97
- const ordered = [...names].sort((left, right) => right.length - left.length);
98
- // The lookbehind keeps the optional '@' from consuming the separator of an
99
- // email-like occurrence: the character before the name (or before its '@')
100
- // must not be a letter, digit, underscore, or another '@'.
101
- const pattern = new RegExp(`(?<![\\p{L}\\p{N}_@])@?(?:${ordered.map(name => `(${escapeRegExp(name)})`).join('|')})(?=$|[^\\p{L}\\p{N}_])`, 'giu');
102
96
  const segments = [];
103
97
  const matched = new Set();
104
98
  let cursor = 0;
105
- for (const match of text.matchAll(pattern)) {
106
- const groupIndex = match.findIndex((group, index) => index >= 1 && group !== undefined);
107
- if (groupIndex < 1)
108
- continue;
109
- if (match.index > cursor)
110
- segments.push({ text: text.slice(cursor, match.index), mention: false });
111
- const name = ordered[groupIndex - 1];
112
- segments.push({ text: `@${name}`, mention: true, name });
113
- matched.add(name.toLowerCase());
114
- cursor = match.index + match[0].length;
99
+ for (const match of scanBodyHandles(text, names)) {
100
+ if (match.start > cursor)
101
+ segments.push({ text: text.slice(cursor, match.start), mention: false });
102
+ segments.push({ text: `@${match.handle}`, mention: true, name: match.handle });
103
+ matched.add(match.handle.toLowerCase());
104
+ cursor = match.end;
115
105
  }
116
106
  if (cursor < text.length)
117
107
  segments.push({ text: text.slice(cursor), mention: false });
118
108
  return { segments, unmatched: names.filter(name => !matched.has(name.toLowerCase())) };
119
109
  }
110
+ /**
111
+ * Whether one draft spells a handle as an authored `@mention`: the shared
112
+ * delivery scan, so the composer's recipient prune and will-notify preview agree
113
+ * with the Host on what the draft actually calls.
114
+ */
115
+ export function containsMention(body, handle) {
116
+ return scanBodyHandles(body, [handle]).length > 0;
117
+ }
118
+ /** Whether one draft carries the `@all` marker the mention menu expands. */
119
+ export function containsAllMention(body) {
120
+ return hasAllMarker(body);
121
+ }
122
+ /**
123
+ * Every Member the mention menu's `@all` row stands for: the roster this
124
+ * composer was handed minus Members who cannot take a Message right now — the
125
+ * same filter the per-handle rows apply, so the expansion and the menu agree.
126
+ */
127
+ export function allMentionMembers(members) {
128
+ return members.filter(status => status.presence !== 'unavailable' && status.member.state !== 'inactive' && status.member.state !== 'archived');
129
+ }
130
+ /**
131
+ * Member ids one draft asks to notify by text alone: the Client's preview of
132
+ * the Host's own body mention resolution, so a hand-typed `@Handle` reports
133
+ * exactly like a pick from the mention menu. `@all` stands for the menu's
134
+ * expansion; a written handle counts whenever its Member can still take a
135
+ * Message — state decides, not presence, because an offline Member is notified
136
+ * and reads it later.
137
+ *
138
+ * The result is preview-only. Only picked recipients travel as explicit
139
+ * recipients, where a name the Channel cannot reach would be a rejected target
140
+ * rather than the prose the Host reads.
141
+ */
142
+ export function mentionedMemberIds(body, members) {
143
+ if (hasAllMarker(body))
144
+ return allMentionMembers(members).map(status => status.member.memberId);
145
+ const candidates = members
146
+ .filter(status => status.member.state !== 'inactive' && status.member.state !== 'archived')
147
+ .map(status => ({ memberId: status.member.memberId, handle: status.member.handle }));
148
+ // The draft's author is the Human reader, and a Message never mentions its
149
+ // own author: resolving with that sender drops a self-call exactly where
150
+ // delivery would.
151
+ return resolveBodyMentions(body, candidates, 'member:human').memberIds;
152
+ }
120
153
  /** Canonical chip handles for one Message's structured mention refs. */
121
154
  export function mentionNamesOf(mentions, handles) {
122
155
  return mentions
@@ -125,6 +158,10 @@ export function mentionNamesOf(mentions, handles) {
125
158
  .map(memberId => memberId === 'member:human' ? 'human' : handles.get(memberId))
126
159
  .filter((name) => name !== undefined);
127
160
  }
161
+ /** Accessible label for one "who is on this work" stack: its owners' handles, comma-separated. */
162
+ export function claimersLabel(owners, t) {
163
+ return t('claimers', { names: owners.map(owner => `@${owner.name}`).join(', ') });
164
+ }
128
165
  const MARKDOWN_BLOCK_CONSTRUCT = /(^|\n)[ \t]{0,3}(?:#{1,6}[ \t]|>[ \t]|[-*+][ \t]|\d+[.)][ \t])|^[ \t]*\|.+\|/m;
129
166
  const MARKDOWN_INLINE_CONSTRUCT = /[`*_[\]!]|~~~|```/;
130
167
  /**
@@ -168,19 +205,23 @@ function calendarDayDelta(at, now) {
168
205
  return Math.round((midnight(now) - midnight(at)) / 86_400_000);
169
206
  }
170
207
  /**
171
- * Queue label for one Inbox row's newest mention. The mention queue scans by
172
- * recency, so the two days a reader reasons about by name are named ("今天" /
173
- * "昨天") while everything older keeps the Message date form, so the row and
174
- * the Thread it opens agree about the same instant.
208
+ * Recency label for one Inbox row's newest fact, shared with the Channel feed's
209
+ * entry line so the two agree about the same instant. Today is a bare clock time
210
+ * the reader is in today, and 「今天」 printed down every row spends the label's
211
+ * first word on the one segment that never varies, while `HH:mm` alone still
212
+ * reads as a clock because it is exactly one. The first day that is not today is
213
+ * the fact the reader has to be told, so it keeps its word; everything older
214
+ * keeps the Message date form, so the row and the Thread it opens agree.
175
215
  */
176
216
  export function formatInboxTime(occurredAt, t, now = new Date()) {
177
217
  const at = new Date(occurredAt);
178
218
  if (Number.isNaN(at.getTime()))
179
219
  return '';
180
220
  const days = calendarDayDelta(at, now);
181
- if (days === 0 || days === 1) {
182
- return `${t(days === 0 ? 'inboxTimeToday' : 'inboxTimeYesterday')} ${pad(at.getHours())}:${pad(at.getMinutes())}`;
183
- }
221
+ if (days === 0)
222
+ return `${pad(at.getHours())}:${pad(at.getMinutes())}`;
223
+ if (days === 1)
224
+ return `${t('inboxTimeYesterday')} ${pad(at.getHours())}:${pad(at.getMinutes())}`;
184
225
  return formatMessageTime(occurredAt, now);
185
226
  }
186
227
  export function formatActivity(activity, options) {
@@ -7,7 +7,7 @@ Model-facing tools for Agent Team Members. The package registers tools in the ca
7
7
  ## Tools
8
8
 
9
9
  - `team_inbox` lists bounded unread Thread summaries for the calling Member, ordered by direct work and then recency; Task standing appears only where the Thread has a real Task overlay. The header states total unread/direct counts and the number of Threads shown (with a truncation conclusion when unread work lies beyond the bounded list); each item shows its exact unread/direct counts, Channel ref, and Task standing. The footer routes reading to `team_thread read`; the render carries no revision and no write token.
10
- - `team_thread` reads a Thread, pages history, follows, or unfollows. `threadRef` is primary; a `taskRef` is a Host alias only for released task-only Clients. The five actions render separately: `status`/`follow`/`unfollow` answer only the Attention question in one line; `read` renders outcome, Thread identity, orientation (full anchor when Host-supplied background is present, bounded anchor subject otherwise, never a duplicate of an in-facts anchor), active Claims only, the chronological facts with inline unread/direct markers, and a read-through/remaining-unread footer; `history` renders a historical page with the full anchor on a first page and the bounded subject on continuation pages, never current Claims or advice. `read` advances the durable watermark and is the only read-side source of a next-write token — rendered only when no unread remains; your own committed public mutations hand off a fresh token as well. A read with nothing unread for the caller advances nothing, writes no operation, and renders `Read — no unread updates …` instead of claiming a commit, still answering from the current projection. Activity facts render structured — actor, Task ref, and the Claim refs each activity claimed, completed, accepted, or released. A read that acknowledges an unread acceptance of a still-done Task carries one `contextAdvice` block (usage, route budgets, the `min(128_000, handoffAt)` Task-boundary threshold, and one keep/rollover/handoff-now action); it is advisory only and degrades to an explicit `unavailable` line when measurement fails.
10
+ - `team_thread` reads a Thread, pages history, follows, or unfollows. `threadRef` is primary; a `taskRef` is a Host alias only for released task-only Clients. The five actions render separately: `status`/`follow`/`unfollow` answer only the Attention question in one line; `read` renders outcome, Thread identity, orientation (full anchor when Host-supplied background is present, bounded anchor subject otherwise — and no background at all for a Member re-entering a Thread it has already read, so its orientation there is that subject plus this batch — never a duplicate of an in-facts anchor), active Claims only, the chronological facts with inline unread/direct markers, and a read-through/remaining-unread footer that also states how many Thread facts precede the watermark whenever the read left any behind; `history` renders a historical page with the full anchor on a first page and the bounded subject on continuation pages, never current Claims or advice. `read` advances the durable watermark and is the only read-side source of a next-write token — rendered only when no unread remains; your own committed public mutations hand off a fresh token as well. A read with nothing unread for the caller advances nothing, writes no operation, and renders `Read — no unread updates …` instead of claiming a commit, still answering from the current projection. Activity facts render structured — actor, Task ref, and the Claim refs each activity claimed, completed, accepted, or released. A read that acknowledges an unread acceptance of a still-done Task carries one `contextAdvice` block (usage, route budgets, the `min(128_000, handoffAt)` Task-boundary threshold, and one keep/rollover/handoff-now action); it is advisory only and degrades to an explicit `unavailable` line when measurement fails.
11
11
  - `team_message` starts a top-level Thread, replies to an existing Thread, or sends a DM. It defaults to a taskless Thread; pass `asTask: true` for atomic Task creation. A committed start/reply renders one committed-verb outcome (Thread created / reply added) with the Message, Thread, and optional Task refs, and exactly one next-write token hand-off. Replies reject unread work before checking token freshness. Typed rejections begin `Not committed`, keep the structured refs and counts needed to reread and retry, and render no numeric revision or write token — recovery is read-and-reconsider. DMs distinguish `Delivered` from `Recorded, not delivered` (with the delivery note and a no-blind-duplicate warning).
12
12
  - `team_claim` lists or mutates the calling Member's Direction Claims through `list`, `claim`, `done`, and `release`; it applies only to real Tasks. `list` renders the Task/Thread identity with active Claims only (the collision surface) and no write token. A committed mutation names its action (Claim created/completed/released), renders the authoritative affected Claim first, and hands off one next-write token. Rejections share the message rejection form. Direction exclusion uses Unicode NFKC normalization, trim, whitespace compression, and deterministic case folding.
13
13
  - `team_view` is the address book: bounded, membership-authorized Channel, top-level Thread, and Member summaries — current addresses, not a work queue. Threads are the sole paginated catalog: newest-first rows carrying threadRef, Channel ref, a bounded anchor subject, and Task standing inline (never a second Task index, never revision or message count). The cursor pages Thread rows only; continuation pages render Threads alone, and paging reaches every authorized top-level Thread, including taskless and off-page taskful ones.
@@ -15,11 +15,11 @@ Model-facing tools for Agent Team Members. The package registers tools in the ca
15
15
  - `context_checkpoint` records one named checkpoint of the calling Member's current context. The durable checkpoint is the successful `tool/call`+`tool/result` pair the Session projection folds; the returned ref is deterministic from the Member Session identity plus the tool call id. It performs no lifecycle side effect in the tool body and concludes the turn: a checkpoint resolves at its containing turn's end, so the model records it as its final action of a completed unit of work.
16
16
  - `context_timeline` returns one bounded structural view of the Member's context generations across the current Session and its archived ancestor lineage: recorded checkpoints and handoff/Team/compaction boundaries, with restorability reasons where a return cannot be proven safe. Structural only — no transcript content. A fresh `context_rollover` never requires consulting the timeline first.
17
17
 
18
- An Agent cannot silently enroll an unfollowed Agent through a mention; the Host returns `member_not_following`. Human confirmation is a separate Host/Client flow. Closed Tasks reject replies, Claims, and new Attention until a Human reopens them; taskless Threads have no Claim or Task-resolution mutation path.
18
+ Mentions are authored in the Message body, as `@Handle` (the `@` is required, matching ignores case, and `@all` reaches the Channel); there is no recipient parameter. An Agent cannot silently enroll an uninvited Member: naming one this Thread has never carried commits the Message, delivers nothing to that Member, and reports them as undelivered. Human confirmation is a separate Host/Client flow. Closed Tasks reject replies, Claims, and new Attention until a Human reopens them; taskless Threads have no Claim or Task-resolution mutation path.
19
19
 
20
20
  Every agent-facing render carries absolute event instants in the fixed Team coordination zone UTC+8 with an explicit offset (`2026-09-08T17:00:00+08:00`): `team_thread` fact lines and anchors stamp each fact with its committing operation's instant, `team_inbox` rows carry `newestOccurredAt`, `team_view` Thread rows carry `lastActivityAt`, notifications state `Occurred at:`, and committed mutations render `Committed at:` from the receipt. The same stored instant renders byte-identically on every reread path; only absolute timestamps are rendered, never relative text, and sequence and revision — not wall-clock time — remain the ordering authority.
21
21
 
22
- Canonical results expose stable refs, optional Task status, Thread revision, Claim history, Attention, and unread facts as structured fields. The model-visible renders follow the action decision surface: the write token appears only on a fully drained `team_thread read` and a committed public mutation; browsing results (`team_view`, `team_inbox`, status/follow/unfollow, `history`, `team_claim list`) and all typed rejections render no revision and no token. Typed `unread_required` and `stale_revision` results keep the structured fields needed to reread and retry deliberately. Tool execution resolves the exact live `exec.agent`; arguments cannot select or impersonate the actor or Workspace. Write request identity derives from sessionId plus tool callId. `context_rollover` and `context_checkpoint` conclude the Agent turn (a rollover must not be followed by old-generation work, and a checkpoint resolves at its containing turn's end); every other Team tool returns to the model loop.
22
+ Canonical results expose stable refs, optional Task status, Thread revision, Claim history, Attention, and unread facts as structured fields. The model-visible renders follow the action decision surface: the write token appears only on a fully drained `team_thread read` and a committed public mutation; browsing results (`team_view`, `team_inbox`, status/follow/unfollow, `history`, `team_claim list`) and all typed rejections render no revision and no token. Typed `unread_required` and `stale_revision` results keep the structured fields needed to reread and retry deliberately. Tool execution resolves the exact live `exec.agent`; arguments cannot select or impersonate the actor, and the optional `workspace` selector can only pick among that actor's participated Workspaces. Write request identity derives from sessionId plus tool callId. `context_rollover` and `context_checkpoint` conclude the Agent turn (a rollover must not be followed by old-generation work, and a checkpoint resolves at its containing turn's end); every other Team tool returns to the model loop.
23
23
 
24
24
  The complete implemented protocol is documented in [`../../docs/team-collaboration.md`](../../docs/team-collaboration.md).
25
25
 
@@ -7,7 +7,7 @@
7
7
  ## 工具
8
8
 
9
9
  - `team_inbox` 列出调用方 Member 的有界未读 Thread 摘要,先按 direct work、再按最近活动排序;只有 Thread 存在真实 Task overlay 时才出现 Task standing。header 给出 unread/direct 总数与展示的 Threads 数(有界列表之外仍有未读时给出截断结论);每个条目显示精确的 unread/direct 计数、Channel ref 与 Task standing。页脚把阅读指向 `team_thread read`;渲染不携带 revision 与写令牌。
10
- - `team_thread` 读取 Thread、分页历史、follow 或 unfollow。`threadRef` 是主身份;`taskRef` 只对 released task-only Client 是 Host alias。五个 action 分开渲染:`status`/`follow`/`unfollow` 一行只回答 Attention 问题;`read` 渲染结果、Thread 身份、导向(有 Host 提供的 background 时给 full anchor,否则给 bounded anchor subjectanchor 已在 facts 中时绝不重复)、只含 active Claims、带行内 unread/direct 标记的按时间 facts、read-through/剩余未读页脚;`history` 渲染历史页(首页 full anchor、continuation 页 bounded subject,绝无当前 Claims 或 advice)。`read` 推进持久 watermark,是 next-write token 的读侧唯一来源——仅在未读清零时渲染;你自己的已提交 public mutation 也会回传一份新 token。一次对调用者没有未读的 read 什么都不推进、不写任何 operation,渲染也不再声称提交(`Read — no unread updates …`),画面仍来自当前 projection。Activity facts 结构化渲染——actor、Task ref,以及每条 activity claim、完成、接受或 release 的 Claim refs。确认仍为 done 状态 Task 的未读验收的那次 read 附带一段 `contextAdvice`(用量、路由预算、`min(128_000, handoffAt)` 任务边界阈值、keep/rollover/handoff-now 唯一动作);它只是建议,测量失败时降级为显式 `unavailable` 文案。
10
+ - `team_thread` 读取 Thread、分页历史、follow 或 unfollow。`threadRef` 是主身份;`taskRef` 只对 released task-only Client 是 Host alias。五个 action 分开渲染:`status`/`follow`/`unfollow` 一行只回答 Attention 问题;`read` 渲染结果、Thread 身份、导向(有 Host 提供的 background 时给 full anchor,否则给 bounded anchor subject——重新进入自己读过的 Thread 的 Member 完全没有 background,它在那里的定位就是该 subject 加本次 batch——anchor 已在 facts 中时绝不重复)、只含 active Claims、带行内 unread/direct 标记的按时间 facts、read-through/剩余未读页脚(只要这次 read 身后留有 Thread facts,页脚同时给出那些 facts 的数量);`history` 渲染历史页(首页 full anchor、continuation 页 bounded subject,绝无当前 Claims 或 advice)。`read` 推进持久 watermark,是 next-write token 的读侧唯一来源——仅在未读清零时渲染;你自己的已提交 public mutation 也会回传一份新 token。一次对调用者没有未读的 read 什么都不推进、不写任何 operation,渲染也不再声称提交(`Read — no unread updates …`),画面仍来自当前 projection。Activity facts 结构化渲染——actor、Task ref,以及每条 activity claim、完成、接受或 release 的 Claim refs。确认仍为 done 状态 Task 的未读验收的那次 read 附带一段 `contextAdvice`(用量、路由预算、`min(128_000, handoffAt)` 任务边界阈值、keep/rollover/handoff-now 唯一动作);它只是建议,测量失败时降级为显式 `unavailable` 文案。
11
11
  - `team_message` 创建顶层 Thread、回复既有 Thread 或发送 DM。默认创建 taskless Thread;传入 `asTask: true` 才原子创建 Task。commit 的 start/reply 渲染一个 committed-verb 结果(Thread created / reply added),携带 Message、Thread 与可选 Task refs,以及恰好一个 next-write token hand-off。Reply 在检查令牌新鲜度前先拒绝未读工作。类型化拒绝以 `Not committed` 开头,保留重读与审慎重试所需的结构化 refs 与计数,不渲染数字 revision 或写令牌——恢复路径是 read-and-reconsider。DM 区分 `Delivered` 与 `Recorded, not delivered`(携带 delivery note 与禁止盲目重发的警告)。
12
12
  - `team_claim` 通过 `list`、`claim`、`done`、`release` 读取或修改调用方 Member 的 Direction Claim;它只适用于真实 Task。`list` 渲染 Task/Thread 身份与只有 active Claims 的 collision surface,且无写令牌。commit 的 mutation 点名动作(Claim created/completed/released),先渲染权威的受影响 Claim,再 hand-off 一个 next-write token。拒绝与 message 拒绝共享同一形式。Direction 互斥键执行 Unicode NFKC normalization、trim、空白压缩和确定性大小写折叠。
13
13
  - `team_view` 是 address book:有界、按 membership 授权的 Channel、顶层 Thread 与 Member 摘要——是当前的地址簿,不是工作队列。Threads 是唯一分页目录:newest-first 行携带 threadRef、Channel ref、bounded anchor subject 与行内 Task standing(绝无第二个 Task 索引,也不渲染 revision 或消息数)。cursor 只延续 Thread 行;continuation 页只渲染 Threads,翻页可达全部获授权的顶层 Threads,包括 taskless 与不在首页的 taskful。
@@ -15,11 +15,11 @@
15
15
  - `context_checkpoint` 为调用的 Member 记录一个命名的当前上下文 checkpoint。durable checkpoint 就是 Session projection 折叠的成功 `tool/call`+`tool/result` 对;返回的 ref 由 Member Session 身份加 tool call id 确定性派生。工具体不做 lifecycle 副作用,但会结束 turn:checkpoint 在其所属 turn 结束时 resolve,因此模型把它作为一个完整工作单元的最后动作来记录。
16
16
  - `context_timeline` 返回该 Member 跨当前 Session 与已归档祖先 lineage 的上下文代际有界结构视图:已记录的 checkpoints 与 handoff/Team/compaction 边界,无法证明可安全回返的条目携带拒绝原因。仅结构信息——不含任何 transcript 正文。fresh 的 `context_rollover` 从不需要先查 timeline。
17
17
 
18
- Agent 不能通过 mention 静默把另一个 unfollowed Agent 加入 Thread;Host 返回 `member_not_following`。Human confirmation 属于单独的 Host/Client 流程。Closed Task 在 Human reopen 前拒绝 reply、Claim 和新的 Attention;taskless Thread 没有 Claim 或 Task resolution mutation path。
18
+ mention Message 正文里撰写,形如 `@Handle`(`@` 必需、大小写不敏感,`@all` 触达整个 Channel);不存在收件人参数。Agent 无法静默加入未被邀请的 Member:mention 一个该 Thread 从未承载过的 Member 时 Message 照常提交,但不向其送达,并在结果里报告为未送达。Human confirmation 属于单独的 Host/Client 流程。Closed Task 在 Human reopen 前拒绝 reply、Claim 和新的 Attention;taskless Thread 没有 Claim 或 Task resolution mutation path。
19
19
 
20
20
  所有 agent-facing 渲染都在固定的 Team 协调时区 UTC+8 下携带带显式偏移的绝对事件时刻(`2026-09-08T17:00:00+08:00`):`team_thread` 的 fact 行与 anchor 为每条 fact 标注其提交 operation 的时刻,`team_inbox` 行携带 `newestOccurredAt`,`team_view` 的 Thread 行携带 `lastActivityAt`,通知给出 `Occurred at:`,已提交变更从 receipt 渲染 `Committed at:`。同一存储时刻在任何重读路径中渲染完全一致;只渲染绝对 timestamp,绝不出现相对时间文案,且 sequence 与 revision——而非 wall-clock 时间——仍是顺序 authority。
21
21
 
22
- Canonical result 以结构化字段暴露稳定 refs、可选 Task status、Thread revision、Claim history、Attention 和未读 facts。模型可见的渲染遵循 action decision surface:写令牌只出现在未读清零的 `team_thread read` 与一次已提交的 public mutation 上;浏览类结果(`team_view`、`team_inbox`、status/follow/unfollow、`history`、`team_claim list`)与所有类型化拒绝都不渲染 revision 与令牌。类型化的 `unread_required` 与 `stale_revision` 结果保留重新读取和审慎重试所需的结构化字段。工具执行通过准确的 live `exec.agent` 解析 actor;参数不能选择或冒充 actor Workspace。写操作的 request identity 由 sessionId 与 tool callId 派生。`context_rollover` 与 `context_checkpoint` 结束 Agent turn;其余 Team tools 将结果返回模型循环,不主动结束 turn。
22
+ Canonical result 以结构化字段暴露稳定 refs、可选 Task status、Thread revision、Claim history、Attention 和未读 facts。模型可见的渲染遵循 action decision surface:写令牌只出现在未读清零的 `team_thread read` 与一次已提交的 public mutation 上;浏览类结果(`team_view`、`team_inbox`、status/follow/unfollow、`history`、`team_claim list`)与所有类型化拒绝都不渲染 revision 与令牌。类型化的 `unread_required` 与 `stale_revision` 结果保留重新读取和审慎重试所需的结构化字段。工具执行通过准确的 live `exec.agent` 解析 actor;参数不能选择或冒充 actor,可选的 `workspace` 选择器只能在该 actor 已参与的 Workspace 中选择。写操作的 request identity 由 sessionId 与 tool callId 派生。`context_rollover` 与 `context_checkpoint` 结束 Agent turn;其余 Team tools 将结果返回模型循环,不主动结束 turn。
23
23
 
24
24
  完整的已实现协议见 [`../../docs/team-collaboration.md`](../../docs/team-collaboration.md)。
25
25
 
@@ -18,3 +18,22 @@ export function member(agent) {
18
18
  throw new Error('team tool requires an active Team Member');
19
19
  return current;
20
20
  }
21
+ /** A collaboration address, never a cwd or Session switch. */
22
+ export const workspaceParam = {
23
+ type: 'string',
24
+ description: 'Target Workspace id exactly as listed in your participation context or team_view. Required when you participate in more than one Workspace; only a sole participation can be inferred. This does not change your Session cwd.',
25
+ };
26
+ /** Reject ambiguous routing before a tool reads facts or attempts a mutation. */
27
+ export function workspaceOf(args, agent) {
28
+ const workspaces = service(agent).workspacesForAgent(agent);
29
+ const choices = workspaces.map(workspace => workspace.workspaceId).join(', ');
30
+ if (args.workspace === undefined) {
31
+ if (workspaces.length !== 1)
32
+ throw new Error(`workspace is required when participating in multiple Workspaces. Choose: ${choices}`);
33
+ return workspaces[0].workspaceId;
34
+ }
35
+ const selected = workspaces.find(workspace => workspace.workspaceId === args.workspace);
36
+ if (selected === undefined)
37
+ throw new Error(`Not participating in Workspace '${args.workspace}'. Choose: ${choices}`);
38
+ return selected.workspaceId;
39
+ }