@nanmicoder/dsh-agent-teams 0.1.22 → 0.1.24-rc.1

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 (45) hide show
  1. package/README.md +35 -13
  2. package/README_ZH.md +35 -13
  3. package/compatibility.json +6 -2
  4. package/lib/artifacts.js +342 -0
  5. package/lib/capabilities.js +6 -4
  6. package/lib/client/ActivityPanel.js +5 -2
  7. package/lib/client/agent-teams-card-definition.js +100 -6
  8. package/lib/client.js +90 -7
  9. package/lib/client.js.map +1 -1
  10. package/lib/command.js +3 -2
  11. package/lib/git-artifact-stamp.json +2 -2
  12. package/lib/harness-compat.js +139 -24
  13. package/lib/index.js +26 -3
  14. package/lib/lineage.js +198 -0
  15. package/lib/mailbox.js +59 -34
  16. package/lib/members.js +355 -82
  17. package/lib/profiles.js +3 -2
  18. package/lib/quality-gates.js +850 -65
  19. package/lib/scheduler.js +22 -3
  20. package/lib/snapshot.js +22 -1
  21. package/lib/state.js +282 -13
  22. package/lib/tool-policies.js +187 -0
  23. package/lib/tools.js +382 -109
  24. package/lib/types/artifacts.d.ts +66 -0
  25. package/lib/types/client/activity-monitor.d.ts +6 -0
  26. package/lib/types/client/agent-teams-card-definition.d.ts +56 -0
  27. package/lib/types/harness-compat.d.ts +78 -15
  28. package/lib/types/index.d.ts +26 -1
  29. package/lib/types/lineage.d.ts +92 -0
  30. package/lib/types/members.d.ts +113 -2
  31. package/lib/types/profiles.d.ts +6 -0
  32. package/lib/types/quality-gates.d.ts +216 -10
  33. package/lib/types/scheduler.d.ts +2 -1
  34. package/lib/types/snapshot.d.ts +20 -0
  35. package/lib/types/state.d.ts +22 -5
  36. package/lib/types/tool-policies.d.ts +119 -0
  37. package/lib/types/tools.d.ts +12 -1
  38. package/lib/types/types.d.ts +100 -0
  39. package/lib/types/workspace-audit.d.ts +27 -0
  40. package/lib/workspace-audit.js +52 -0
  41. package/package.json +61 -54
  42. package/pnpm-workspace.yaml +304 -273
  43. package/release-notes/v0.1.23.md +34 -0
  44. package/release-notes/v0.1.24-rc.1.md +13 -0
  45. package/scripts/legacy-session-source-repair.mjs +344 -0
package/lib/mailbox.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { join } from 'node:path';
2
2
  import { acknowledgeMailbox, CAPTAIN_KEY, discardMailboxMessages, findTeamByCaptain, readMailbox, readUnreadMailbox, readTeam, sanitizeKey, withTeamLock } from "./state.js";
3
3
  import { sessionOwnEvents } from "./harness-compat.js";
4
+ import { leadIdentityOfAsync, sessionLeadsTeamAsync } from "./lineage.js";
4
5
  const PREFIX = 'AgentTeams inbox receipt: ';
5
6
  export function isCurrentMail(team, message) {
6
7
  if (message.discardedAt !== undefined)
@@ -75,26 +76,35 @@ export function installMailboxAdmission(ctx, stateDir) {
75
76
  }));
76
77
  const redundantSettlements = new Set();
77
78
  if (successfulSettlements.size > 0) {
78
- const owned = await findTeamByCaptain(root, payload.agent.id);
79
- if (owned !== undefined)
80
- await withTeamLock(`team:${root}:${owned.id}`, async () => {
81
- const team = await readTeam(root, owned.id);
82
- if (team?.captainSessionId !== payload.agent.id || team.halted)
83
- return;
84
- const reports = await readMailbox(root, team.id, CAPTAIN_KEY);
85
- for (const member of team.members) {
86
- if (!successfulSettlements.has(member.id) || member.status === 'removed' || member.stopping)
87
- continue;
88
- const tasks = team.tasks.filter(task => task.assignee === member.name);
89
- if (tasks.some(task => task.status === 'claimed' || task.status === 'in_progress'))
90
- continue;
91
- const latest = tasks.filter(task => (task.attempt ?? 0) > 0 || task.status === 'completed').sort((a, b) => b.updatedAt - a.updatedAt)[0];
92
- if (latest?.status === 'completed' && reports.some(report => report.from === member.name && report.ts >= latest.updatedAt
93
- && report.discardedAt === undefined && (report.deliveredAt !== undefined || report.readAt !== undefined))) {
94
- redundantSettlements.add(member.id);
79
+ // Narrow containment: only AgentTeams state reads degrade silently.
80
+ try {
81
+ const owned = await findTeamByCaptain(root, payload.agent.id, await leadIdentityOfAsync(ctx, payload.agent.session));
82
+ if (owned !== undefined)
83
+ await withTeamLock(`team:${root}:${owned.id}`, async () => {
84
+ const team = await readTeam(root, owned.id);
85
+ if (team === undefined || !(await sessionLeadsTeamAsync(ctx, team, payload.agent.id)) || team.halted)
86
+ return;
87
+ const reports = await readMailbox(root, team.id, CAPTAIN_KEY);
88
+ for (const member of team.members) {
89
+ if (!successfulSettlements.has(member.id) || member.status === 'removed' || member.stopping)
90
+ continue;
91
+ const tasks = team.tasks.filter(task => task.assignee === member.name);
92
+ if (tasks.some(task => task.status === 'claimed' || task.status === 'in_progress'))
93
+ continue;
94
+ const latest = tasks.filter(task => (task.attempt ?? 0) > 0 || task.status === 'completed').sort((a, b) => b.updatedAt - a.updatedAt)[0];
95
+ if (latest?.status === 'completed' && reports.some(report => report.from === member.name && report.ts >= latest.updatedAt
96
+ && report.discardedAt === undefined && (report.deliveredAt !== undefined || report.readAt !== undefined))) {
97
+ redundantSettlements.add(member.id);
98
+ }
95
99
  }
96
- }
97
- });
100
+ });
101
+ }
102
+ catch (error) {
103
+ // A corrupt AgentTeams state file (e.g. an invalid team.json) must never
104
+ // hold the host's step admission hostage: degrade to plain admission.
105
+ ctx.logger?.warn(`agent-teams: mailbox admission degraded to plain pass-through for agent ${payload.agent.id}: could not read team state under ${root} (a team.json may be corrupt or unreadable): ${String(error)}`);
106
+ return decision;
107
+ }
98
108
  }
99
109
  const batches = new Map();
100
110
  const inputs = decision.messages.map(input => {
@@ -116,27 +126,42 @@ export function installMailboxAdmission(ctx, stateDir) {
116
126
  // One read/acknowledgement per recipient per step, even for a large burst.
117
127
  for (const batch of batches.values()) {
118
128
  const { receipt } = batch;
119
- await withTeamLock(`team:${root}:${receipt.teamId}`, async () => {
120
- const team = await readTeam(root, receipt.teamId);
121
- if (team === undefined)
122
- return;
123
- const recipientId = receipt.recipient === CAPTAIN_KEY ? team.captainSessionId
124
- : team.members.find(member => member.name === receipt.recipient && member.status !== 'removed' && member.stopping !== true)?.id;
125
- if (recipientId !== payload.agent.id)
126
- return;
127
- const selected = (await readMailbox(root, team.id, receipt.recipient)).filter(message => batch.ids.has(message.id));
128
- const current = selected.filter(message => message.readAt === undefined && isCurrentMail(team, message));
129
- await discardMailboxMessages(root, team.id, receipt.recipient, selected.filter(message => !isCurrentMail(team, message)).map(message => message.id));
130
- await acknowledgeMailbox(root, team.id, receipt.recipient, current.map(message => message.id));
131
- batch.current = new Map(current.map(message => [message.id, message]));
132
- });
129
+ try {
130
+ await withTeamLock(`team:${root}:${receipt.teamId}`, async () => {
131
+ const team = await readTeam(root, receipt.teamId);
132
+ if (team === undefined)
133
+ return;
134
+ // Captain mail is admitted for any lead session of the team: after a
135
+ // fork the human reads it in the fork while members still address the
136
+ // recorded captain. Acknowledging marks it read for both.
137
+ const isCaptainRecipient = receipt.recipient === CAPTAIN_KEY
138
+ && await sessionLeadsTeamAsync(ctx, team, payload.agent.id);
139
+ const recipientId = receipt.recipient === CAPTAIN_KEY ? team.captainSessionId
140
+ : team.members.find(member => member.name === receipt.recipient && member.status !== 'removed' && member.stopping !== true)?.id;
141
+ if (!isCaptainRecipient && recipientId !== payload.agent.id)
142
+ return;
143
+ const selected = (await readMailbox(root, team.id, receipt.recipient)).filter(message => batch.ids.has(message.id));
144
+ const current = selected.filter(message => message.readAt === undefined && isCurrentMail(team, message));
145
+ await discardMailboxMessages(root, team.id, receipt.recipient, selected.filter(message => !isCurrentMail(team, message)).map(message => message.id));
146
+ await acknowledgeMailbox(root, team.id, receipt.recipient, current.map(message => message.id));
147
+ batch.current = new Map(current.map(message => [message.id, message]));
148
+ });
149
+ }
150
+ catch (error) {
151
+ // Narrow containment, per team: the messages of this batch are not
152
+ // acked (the teamId/recipient above identify the unread mail), but the
153
+ // receipt inputs still pass through untouched instead of holding the
154
+ // host's step admission hostage.
155
+ batch.failed = true;
156
+ ctx.logger?.warn(`agent-teams: mailbox admission degraded to plain pass-through for agent ${payload.agent.id}: could not read team "${receipt.teamId}" (state under ${root} may be corrupt or unreadable, recipient ${receipt.recipient} mail left unacked): ${String(error)}`);
157
+ }
133
158
  }
134
159
  const admitted = [];
135
160
  for (const { input, receipt, batch } of inputs) {
136
161
  if (input.source?.kind === 'subagent-settled' && redundantSettlements.has(input.source.senderSessionId)
137
162
  && input.source.summary === `Background subagent ${input.source.senderSessionId} finished and will do no further work unless you send it more.`)
138
163
  continue;
139
- if (receipt === undefined || batch === undefined) {
164
+ if (receipt === undefined || batch === undefined || batch.failed === true) {
140
165
  admitted.push(input);
141
166
  continue;
142
167
  }