@wowyuarm/dsh-agent-team 0.1.4 → 0.1.5

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 (56) hide show
  1. package/README.zh.md +5 -2
  2. package/package.json +1 -1
  3. package/packages/agent-team/lib/auto-compaction.js +39 -1
  4. package/packages/agent-team/lib/index.js +85 -5
  5. package/packages/agent-team/lib/ledger.js +80 -2
  6. package/packages/agent-team/lib/spec.js +12 -0
  7. package/packages/agent-team/lib/typert.host.js +94 -22
  8. package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
  9. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  10. package/packages/agent-team/lib/typert.remote-client.js +79 -22
  11. package/packages/agent-team/lib/types/auto-compaction.d.ts +9 -0
  12. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -1
  13. package/packages/agent-team/lib/types/index.d.ts +13 -1
  14. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  15. package/packages/agent-team/lib/types/ledger.d.ts +20 -0
  16. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  17. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  18. package/packages/agent-team/lib/types/types.d.ts +41 -1
  19. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  20. package/packages/client-agent-team/lib/client.js +486 -90
  21. package/packages/client-agent-team/lib/client.js.map +1 -1
  22. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +2 -1
  23. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  24. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +41 -5
  25. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  26. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +20 -0
  27. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  28. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +3 -1
  29. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  30. package/packages/client-agent-team/lib/types/client/TeamComposer.js +73 -17
  31. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -1
  32. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +16 -1
  33. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  34. package/packages/client-agent-team/lib/types/client/TeamMessage.js +48 -18
  35. package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts +3 -2
  36. package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts.map +1 -1
  37. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts +5 -2
  38. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.js +2 -4
  40. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +21 -2
  42. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  43. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  44. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +4 -2
  45. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/index.js +15 -13
  47. package/packages/client-agent-team/lib/types/client/locales.d.ts +16 -0
  48. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  49. package/packages/client-agent-team/lib/types/client/locales.js +16 -0
  50. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts +16 -0
  51. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts.map +1 -0
  52. package/packages/client-agent-team/lib/types/client/sidebar-sections.js +102 -0
  53. package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -1
  54. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  56. package/packages/client-agent-team/lib/types/client/team-formatters.js +9 -2
package/README.zh.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/@wowyuarm/dsh-agent-team?style=flat-square)](https://www.npmjs.com/package/@wowyuarm/dsh-agent-team)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
7
+ [![Release](https://img.shields.io/github/v/release/wowyuarm/dsh-agent-team?include_prereleases&style=flat-square)](https://github.com/wowyuarm/dsh-agent-team/releases)
5
8
  [![Listed on Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com/p/wowyuarm/dsh-agent-team/)
6
9
 
7
10
  **dsh-agent-team** 帮助 human 有序管理任务、把 agents 用成真正的协作者:Agent 是 session 的持久身份;Workspace 按项目组织 agents 与 sessions;Channel 承载职责分派;Task Thread 把多个 session agent 串成一条推进线。
@@ -98,7 +101,7 @@ dsh web
98
101
 
99
102
  ## 开发
100
103
 
101
- 维护中的文档入口是 [`docs/README.md`](docs/README.md)。常用检查命令:
104
+ 维护中的文档入口是 [`docs/README.zh.md`](docs/README.zh.md)。常用检查命令:
102
105
 
103
106
  ```sh
104
107
  corepack pnpm install
@@ -112,7 +115,7 @@ npm pack --dry-run
112
115
 
113
116
  `npm run test:browser` 使用相邻的 `../deepseek-harness` checkout、隔离临时 profile 和 `/usr/bin/google-chrome`(可用 `CHROME_PATH` 覆盖),不需要 provider credentials。手动检查时,`npm run preview:ui` 会加载不调用模型的 Team fixture;`DEEPSEEK_API_KEY=... npm run preview` 会启动真实 provider preview。两个 preview 命令都会在 `Ctrl+C` 后清理临时状态。
114
117
 
115
- 架构和协作协议见 [`docs/architecture.md`](docs/architecture.md) 与 [`docs/team-collaboration.md`](docs/team-collaboration.md)。
118
+ 架构和协作协议见 [`docs/architecture.zh.md`](docs/architecture.zh.md) 与 [`docs/team-collaboration.zh.md`](docs/team-collaboration.zh.md)。
116
119
 
117
120
  ## 许可证
118
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wowyuarm/dsh-agent-team",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Help humans organize tasks and let agents collaborate - a Team plugin for DeepSeek Harness",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,15 @@
1
1
  import { ManualCompactionError } from '@deepseek-ai/dsh-compaction';
2
2
  /** Strict threshold: 200K exactly does not compact. */
3
3
  export const AUTO_COMPACTION_TOKEN_LIMIT = 200_000;
4
+ /**
5
+ * Suggested summary marker of the pre-compaction memory hint. Same shape as
6
+ * the inbox and recovery notices so future turns can recognize it.
7
+ */
8
+ export const PRE_COMPACTION_NOTICE_SUMMARY = 'Compaction is imminent; consider persisting key conclusions.';
9
+ /** Suggested pre-compaction hint body; writing is the Agent's own call. */
10
+ export function preCompactionNoticeText() {
11
+ return 'This session is about to be compacted, which will summarize away the current conversation detail. If there are durable conclusions worth keeping — validated facts, decisions, or reusable details — consider writing them to your memory or notes now. This is only a reminder: decide yourself whether anything is worth persisting, and do nothing if not. Do not start new work because of this message.';
12
+ }
4
13
  /** Claim owners of a durable Human acceptance, including early-completed ones. */
5
14
  export function acceptedTaskCompactionMembers(operation) {
6
15
  if (operation.kind !== 'team/task-changed' || operation.actor.kind !== 'human' || operation.data.activity.kind !== 'accept')
@@ -69,11 +78,40 @@ export class AutoCompactionCoordinator {
69
78
  this.fail(memberId, agent, 'automatic compaction failed: tokenMeter is unavailable in the Member scope');
70
79
  return;
71
80
  }
72
- const before = meter.measure(agent.session).totalTokens;
81
+ let before = meter.measure(agent.session).totalTokens;
73
82
  if (before <= AUTO_COMPACTION_TOKEN_LIMIT) {
74
83
  this.complete(memberId, agent);
75
84
  return;
76
85
  }
86
+ // The session is over the threshold and about to be compacted: steer one
87
+ // advisory hint so the Agent can persist its own key conclusions first.
88
+ // The hint turn itself is ordinary agent work — its failure is contained
89
+ // by the agent loop and never blocks compaction; only the fresh idle
90
+ // boundary is awaited before the final measurement and compactNow.
91
+ if (this.options.steerPreCompaction !== undefined) {
92
+ try {
93
+ this.options.steerPreCompaction(agent);
94
+ }
95
+ catch (error) {
96
+ this.options.log(`pre-compaction memory hint failed: ${describe(error)} (member ${memberId}, session ${agent.id})`);
97
+ }
98
+ try {
99
+ if (!(await waitForIdleOrAbort(agent, this.controller.signal)))
100
+ return;
101
+ }
102
+ catch (error) {
103
+ this.options.log(`pre-compaction hint idle wait failed: ${describe(error)} (member ${memberId}, session ${agent.id})`);
104
+ }
105
+ if (this.controller.signal.aborted)
106
+ return;
107
+ if (this.options.agentForMember(memberId) !== agent)
108
+ continue;
109
+ before = meter.measure(agent.session).totalTokens;
110
+ if (before <= AUTO_COMPACTION_TOKEN_LIMIT) {
111
+ this.complete(memberId, agent);
112
+ return;
113
+ }
114
+ }
77
115
  const engine = this.options.compactionForAgent(agent);
78
116
  if (engine === undefined) {
79
117
  // A roster-subtree teardown (bundle-row reload, config hot-apply)
@@ -52,7 +52,7 @@ import { SessionId } from '@deepseek-ai/dsh-session';
52
52
  import { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
53
53
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
54
54
  import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
55
- import { acceptedTaskCompactionMembers, AutoCompactionCoordinator } from "./auto-compaction.js";
55
+ import { acceptedTaskCompactionMembers, AutoCompactionCoordinator, PRE_COMPACTION_NOTICE_SUMMARY, preCompactionNoticeText } from "./auto-compaction.js";
56
56
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
57
57
  import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
58
58
  import { agentTeamDomainSpec } from "./spec.js";
@@ -96,6 +96,7 @@ let AgentTeam = (() => {
96
96
  let _updateChannel_decorators;
97
97
  let _addMember_decorators;
98
98
  let _recoverMember_decorators;
99
+ let _clearMemberContext_decorators;
99
100
  let _updateMember_decorators;
100
101
  let _changeTask_decorators;
101
102
  let _promoteThread_decorators;
@@ -121,6 +122,7 @@ let AgentTeam = (() => {
121
122
  _updateChannel_decorators = [Remote('updateChannel')];
122
123
  _addMember_decorators = [Remote('addMember')];
123
124
  _recoverMember_decorators = [Remote('recoverMember')];
125
+ _clearMemberContext_decorators = [Remote('clearMemberContext')];
124
126
  _updateMember_decorators = [Remote('updateMember')];
125
127
  _changeTask_decorators = [Remote('changeTask')];
126
128
  _promoteThread_decorators = [Remote('promoteThread')];
@@ -143,6 +145,7 @@ let AgentTeam = (() => {
143
145
  __esDecorate(this, null, _updateChannel_decorators, { kind: "method", name: "updateChannel", static: false, private: false, access: { has: obj => "updateChannel" in obj, get: obj => obj.updateChannel }, metadata: _metadata }, null, _instanceExtraInitializers);
144
146
  __esDecorate(this, null, _addMember_decorators, { kind: "method", name: "addMember", static: false, private: false, access: { has: obj => "addMember" in obj, get: obj => obj.addMember }, metadata: _metadata }, null, _instanceExtraInitializers);
145
147
  __esDecorate(this, null, _recoverMember_decorators, { kind: "method", name: "recoverMember", static: false, private: false, access: { has: obj => "recoverMember" in obj, get: obj => obj.recoverMember }, metadata: _metadata }, null, _instanceExtraInitializers);
148
+ __esDecorate(this, null, _clearMemberContext_decorators, { kind: "method", name: "clearMemberContext", static: false, private: false, access: { has: obj => "clearMemberContext" in obj, get: obj => obj.clearMemberContext }, metadata: _metadata }, null, _instanceExtraInitializers);
146
149
  __esDecorate(this, null, _updateMember_decorators, { kind: "method", name: "updateMember", static: false, private: false, access: { has: obj => "updateMember" in obj, get: obj => obj.updateMember }, metadata: _metadata }, null, _instanceExtraInitializers);
147
150
  __esDecorate(this, null, _changeTask_decorators, { kind: "method", name: "changeTask", static: false, private: false, access: { has: obj => "changeTask" in obj, get: obj => obj.changeTask }, metadata: _metadata }, null, _instanceExtraInitializers);
148
151
  __esDecorate(this, null, _promoteThread_decorators, { kind: "method", name: "promoteThread", static: false, private: false, access: { has: obj => "promoteThread" in obj, get: obj => obj.promoteThread }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -216,6 +219,14 @@ let AgentTeam = (() => {
216
219
  this.emitAutoCompactionChanged(memberId);
217
220
  },
218
221
  log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
222
+ // Advisory only: the Agent decides whether anything is worth persisting.
223
+ steerPreCompaction: agent => {
224
+ const hint = createUserMessage({
225
+ content: [{ type: 'text', text: preCompactionNoticeText() }],
226
+ source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: PRE_COMPACTION_NOTICE_SUMMARY },
227
+ });
228
+ agent.steer(hint);
229
+ },
219
230
  });
220
231
  }
221
232
  /** Open the durable ledger and restore every enabled Member independently. */
@@ -246,8 +257,15 @@ let AgentTeam = (() => {
246
257
  this.notifyMember(agent);
247
258
  }
248
259
  // A turn that ends without an error closes any automatic recovery episode.
249
- if (status === 'idle' && member !== undefined && this.memberFailures.get(member.memberId)?.runtime === undefined) {
250
- this.recovery.onCleanTurnEnd(member.memberId);
260
+ // The idle transition is itself presence-affecting (working available),
261
+ // so it must wake workspace watchers exactly like the running transition
262
+ // above; without this wake, cached Client member rows keep showing the
263
+ // Member as working after every turn until an unrelated change arrives.
264
+ if (status === 'idle' && member !== undefined) {
265
+ if (this.memberFailures.get(member.memberId)?.runtime === undefined) {
266
+ this.recovery.onCleanTurnEnd(member.memberId);
267
+ }
268
+ this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
251
269
  }
252
270
  });
253
271
  const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
@@ -463,6 +481,66 @@ let AgentTeam = (() => {
463
481
  this.steerResume(member, this.manualResumeText());
464
482
  return Object.freeze({ status: this.memberStatus(member) });
465
483
  }
484
+ /**
485
+ * Start one enabled Member from a new context: dispose the live handle,
486
+ * archive the previous Session (its log stays on disk for history), and
487
+ * activate a fresh Session under a new sessionId, so preset, tools, private
488
+ * memory, and model selection all reload while the next turn carries no
489
+ * history. The durable operation moves the Member's sessionId; identity,
490
+ * memory path, and binding survive. A new id is what keeps the Web Client
491
+ * seat live: a disposed generation's resident instance keeps its `removed`
492
+ * bit forever, so renewing under the same id would leave a permanently
493
+ * grayed session view.
494
+ */
495
+ async clearMemberContext(request) {
496
+ return this.enqueueLifecycle(async () => {
497
+ this.requireAccepting();
498
+ this.requireWorkspace(request.workspaceId);
499
+ const stored = this.requireLedger().getMember(request.memberId);
500
+ if (stored === undefined || stored.workspaceId !== request.workspaceId)
501
+ throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
502
+ if (stored.state !== 'enabled')
503
+ throw new Error(`Agent Member '${stored.handle}' is ${stored.state}; only enabled Members can start from a new context`);
504
+ const active = this.handles.get(request.memberId);
505
+ if (active === undefined)
506
+ throw new Error(`Agent Member '${stored.handle}' has no active session to clear`);
507
+ if (this.runningAgents.has(active.agent.id))
508
+ throw new Error(`Agent Member '${stored.handle}' is still running; wait for the current turn to end before starting from a new context`);
509
+ const previousSessionId = stored.sessionId;
510
+ // The fresh id derives from the requestId, so a retried identical
511
+ // request mints the same id and the ledger dedupes it instead of
512
+ // colliding; the format matches addMember's `agent-team-<uuid>`.
513
+ const sessionId = SessionId(`agent-team-${request.requestId}`);
514
+ const result = await this.requireLedger().renewMemberSession({ ...request, sessionId, actor: agentTeamHumanActor() });
515
+ if (result.committed)
516
+ this.emitCommitted(result.value.receipt);
517
+ else {
518
+ // A retried identical request already renewed this Member; report the
519
+ // recorded outcome without another dispose/reactivate cycle.
520
+ return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
521
+ }
522
+ const renewed = result.value.member;
523
+ // Drop the old handle's transient state: pending recovery episodes and
524
+ // error markers belong to the disposed agent, not to the Member.
525
+ this.recovery.stopTracking(request.memberId);
526
+ await active.dispose();
527
+ this.handles.delete(request.memberId);
528
+ this.modelSelections.delete(request.memberId);
529
+ this.clearMemberFailure(request.memberId, 'activation');
530
+ this.clearMemberNotificationState(request.memberId);
531
+ // The previous log survives on disk; archiving hides it from every
532
+ // grouping surface so one Member keeps exactly one visible Session.
533
+ await this.ctx.workspaceRegistry.archiveSession(previousSessionId);
534
+ await this.activateMember(renewed, undefined, undefined, previousSessionId);
535
+ const reactivated = this.handles.get(request.memberId);
536
+ if (reactivated === undefined) {
537
+ // Reactivation failed; the activation diagnostic carries the reason and
538
+ // the durable renewal stays honest about the attempt.
539
+ throw new Error(`Agent Member '${stored.handle}' failed to start a new context: ${this.memberFailures.get(request.memberId)?.activation ?? 'unknown error'}`);
540
+ }
541
+ return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(renewed) });
542
+ });
543
+ }
466
544
  /** Ledger handle for log lines; falls back to the raw id when unknown. */
467
545
  memberLabel(memberId) {
468
546
  return this.ledger?.getMember(memberId)?.handle ?? memberId;
@@ -831,7 +909,7 @@ let AgentTeam = (() => {
831
909
  throw error;
832
910
  }
833
911
  }
834
- async activateMember(member, knownWorkspacePath, knownSessions) {
912
+ async activateMember(member, knownWorkspacePath, knownSessions, forkedFrom) {
835
913
  if (this.handles.has(member.memberId))
836
914
  return;
837
915
  let created;
@@ -865,7 +943,9 @@ let AgentTeam = (() => {
865
943
  ? await this.ctx.agents.resume({ resumeSessionId: member.sessionId, agentOptions, setup })
866
944
  : await this.ctx.agents.create({
867
945
  sessionId: member.sessionId,
868
- meta: { cwd: workspacePath, agentPreset: member.presetId },
946
+ // A context renewal records its fork lineage so the archived
947
+ // previous Session stays discoverable from the durable header.
948
+ meta: { cwd: workspacePath, agentPreset: member.presetId, ...(forkedFrom === undefined ? {} : { parentSession: forkedFrom }) },
869
949
  agentOptions,
870
950
  setup,
871
951
  });
@@ -195,6 +195,35 @@ export class AgentTeamLedger {
195
195
  resumeMember(request) {
196
196
  return this.setMemberState(request, 'enabled');
197
197
  }
198
+ /**
199
+ * Move one enabled Member onto a fresh Session: the durable operation
200
+ * records the sessionId transition while identity, memory, and binding
201
+ * survive. The Host disposes the old handle and archives the previous
202
+ * Session log around this write, so the next turn starts empty.
203
+ */
204
+ renewMemberSession(request) {
205
+ return this.enqueue(async () => {
206
+ const existing = this.state.byRequest.get(request.requestId);
207
+ if (existing !== undefined) {
208
+ this.assertSameMemberSessionRenewed(existing, request);
209
+ return this.resolved(this.memberResult(existing));
210
+ }
211
+ this.assertHumanActor(request.actor);
212
+ const prior = this.requireMember(request.memberId);
213
+ if (prior.state !== 'enabled')
214
+ throw new Error(`Agent Member '${prior.handle}' is ${prior.state}; only enabled Members can start from a new context`);
215
+ if (prior.sessionId === request.sessionId)
216
+ throw new Error(`Agent Member '${prior.handle}' already runs Session '${request.sessionId}'`);
217
+ const member = Object.freeze({ ...prior, sessionId: request.sessionId });
218
+ const operation = Object.freeze({
219
+ ...this.operationBase(request, this.nextSequence()), kind: 'team/member-session-renewed',
220
+ data: Object.freeze({ member, previousSessionId: prior.sessionId }),
221
+ });
222
+ await this.table.put(operation.operationId, operation);
223
+ this.apply(operation);
224
+ return this.committed(this.memberResult(operation));
225
+ });
226
+ }
198
227
  getMember(memberId) {
199
228
  return this.state.members.get(memberId);
200
229
  }
@@ -481,8 +510,10 @@ export class AgentTeamLedger {
481
510
  // Acceptance normally waits for every Claim to finish (in_review);
482
511
  // a Human may also accept early while work is in progress, which then
483
512
  // completes the still-active Claims inside the same atomic operation.
484
- if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress') {
485
- throw new Error(`Task '${task.taskRef}' must be in_review or in_progress before acceptance`);
513
+ // A never-claimed todo Task may be accepted directly as well: work
514
+ // finished outside the ledger is declared complete by the Human.
515
+ if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress' && task.status !== 'todo') {
516
+ throw new Error(`Task '${task.taskRef}' must be in_review, in_progress, or todo before acceptance`);
486
517
  }
487
518
  const priorActiveClaims = [...this.state.claims.values()].filter(claim => claim.taskRef === task.taskRef && claim.state === 'active');
488
519
  if (request.action === 'accept' && task.status === 'in_progress' && priorActiveClaims.length === 0) {
@@ -860,6 +891,8 @@ export class AgentTeamLedger {
860
891
  case 'team/member-suspended':
861
892
  case 'team/member-resumed':
862
893
  case 'team/member-session-restarted':
894
+ case 'team/member-context-cleared':
895
+ case 'team/member-session-renewed':
863
896
  case 'team/member-updated':
864
897
  case 'team/member-removed':
865
898
  return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
@@ -949,6 +982,8 @@ export class AgentTeamLedger {
949
982
  case 'team/member-suspended':
950
983
  case 'team/member-resumed':
951
984
  case 'team/member-session-restarted':
985
+ case 'team/member-context-cleared':
986
+ case 'team/member-session-renewed':
952
987
  case 'team/member-updated':
953
988
  case 'team/channel-member-added':
954
989
  case 'team/thread-read':
@@ -1058,6 +1093,28 @@ export class AgentTeamLedger {
1058
1093
  throw new Error('invalid Member restart');
1059
1094
  return;
1060
1095
  }
1096
+ if (operation.kind === 'team/member-context-cleared') {
1097
+ assertHuman();
1098
+ const prior = projection.members.get(operation.data.member.memberId);
1099
+ // Legacy in-place clear: the recorded Member is unchanged, and the
1100
+ // projection deliberately does not change either.
1101
+ if (prior === undefined || prior.state !== 'enabled' || !this.sameMemberIdentity(prior, operation.data.member))
1102
+ throw new Error('invalid Member context clear');
1103
+ return;
1104
+ }
1105
+ if (operation.kind === 'team/member-session-renewed') {
1106
+ assertHuman();
1107
+ const prior = projection.members.get(operation.data.member.memberId);
1108
+ // The renewal moves exactly the sessionId: the prior Member must be
1109
+ // enabled on the recorded previous Session, and every other fact must
1110
+ // carry over unchanged.
1111
+ if (prior === undefined || prior.state !== 'enabled'
1112
+ || prior.sessionId !== operation.data.previousSessionId
1113
+ || operation.data.member.sessionId === operation.data.previousSessionId
1114
+ || !this.sameMemberFacts(prior, operation.data.member))
1115
+ throw new Error('invalid Member session renewal');
1116
+ return;
1117
+ }
1061
1118
  if (operation.kind === 'team/channel-updated') {
1062
1119
  assertHuman();
1063
1120
  const prior = projection.channels.get(operation.data.channel.channelRef);
@@ -1486,6 +1543,15 @@ export class AgentTeamLedger {
1486
1543
  // projection deliberately does not change.
1487
1544
  return;
1488
1545
  }
1546
+ if (operation.kind === 'team/member-context-cleared') {
1547
+ // Legacy audit-only marker: identity, memory path, and binding were
1548
+ // untouched; the Host recreated the live Session empty under the same id.
1549
+ return;
1550
+ }
1551
+ if (operation.kind === 'team/member-session-renewed') {
1552
+ target.members.set(operation.data.member.memberId, operation.data.member);
1553
+ return;
1554
+ }
1489
1555
  if (operation.kind === 'team/channel-updated') {
1490
1556
  target.channels.set(operation.data.channel.channelRef, operation.data.channel);
1491
1557
  return;
@@ -2176,6 +2242,11 @@ export class AgentTeamLedger {
2176
2242
  if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.member.memberId !== request.memberId)
2177
2243
  this.throwRequestCollision(request.requestId);
2178
2244
  }
2245
+ assertSameMemberSessionRenewed(operation, request) {
2246
+ if (operation.kind !== 'team/member-session-renewed' || !this.sameActor(operation.actor, request.actor)
2247
+ || operation.data.member.memberId !== request.memberId || operation.data.member.sessionId !== request.sessionId)
2248
+ this.throwRequestCollision(request.requestId);
2249
+ }
2179
2250
  assertSameChannelUpdate(operation, request) {
2180
2251
  if (operation.kind !== 'team/channel-updated' || !this.sameActor(operation.actor, request.actor)
2181
2252
  || operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef
@@ -2282,6 +2353,13 @@ export class AgentTeamLedger {
2282
2353
  && isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
2283
2354
  && left.privateMemoryPath === right.privateMemoryPath;
2284
2355
  }
2356
+ /** sameMemberIdentity minus the sessionId: every durable Member fact a session renewal must carry over unchanged. */
2357
+ sameMemberFacts(left, right) {
2358
+ return left.memberId === right.memberId && left.workspaceId === right.workspaceId
2359
+ && left.handle === right.handle && left.description === right.description && left.presetId === right.presetId
2360
+ && isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
2361
+ && left.privateMemoryPath === right.privateMemoryPath;
2362
+ }
2285
2363
  sameActor(left, right) {
2286
2364
  return left.kind === right.kind && left.memberId === right.memberId && left.handle === right.handle;
2287
2365
  }
@@ -237,6 +237,18 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
237
237
  kind: z.literal('team/member-session-restarted'),
238
238
  data: z.object({ member: memberSchema }).strict(),
239
239
  }).strict(),
240
+ z.object({
241
+ ...operationBase,
242
+ previousOperationId: operationIdSchema.nullable(),
243
+ kind: z.literal('team/member-context-cleared'),
244
+ data: z.object({ member: memberSchema }).strict(),
245
+ }).strict(),
246
+ z.object({
247
+ ...operationBase,
248
+ previousOperationId: operationIdSchema.nullable(),
249
+ kind: z.literal('team/member-session-renewed'),
250
+ data: z.object({ member: memberSchema, previousSessionId: sessionIdSchema }).strict(),
251
+ }).strict(),
240
252
  z.object({
241
253
  ...operationBase,
242
254
  previousOperationId: operationIdSchema.nullable(),