@wowyuarm/dsh-agent-team 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +4 -11
  2. package/README.zh.md +4 -11
  3. package/package.json +53 -44
  4. package/packages/agent-team/README.md +2 -2
  5. package/packages/agent-team/README.zh.md +2 -1
  6. package/packages/agent-team/lib/context-management.js +8 -5
  7. package/packages/agent-team/lib/context-projection.js +12 -8
  8. package/packages/agent-team/lib/context-source.js +163 -23
  9. package/packages/agent-team/lib/index.js +174 -124
  10. package/packages/agent-team/lib/ledger.js +111 -30
  11. package/packages/agent-team/lib/member-time-context.js +126 -0
  12. package/packages/agent-team/lib/pressure-policy.js +1 -2
  13. package/packages/agent-team/lib/progress-nudge.js +12 -4
  14. package/packages/agent-team/lib/session-remediation.js +481 -0
  15. package/packages/agent-team/lib/spec.js +3 -2
  16. package/packages/agent-team/lib/time-format.js +56 -0
  17. package/packages/agent-team/lib/typert.host.js +82 -49
  18. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  19. package/packages/agent-team/lib/typert.remote-client.js +46 -25
  20. package/packages/agent-team/lib/types/context-management.d.ts +6 -1
  21. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/context-source.d.ts +70 -36
  24. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/index.d.ts +32 -7
  26. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/ledger.d.ts +25 -0
  28. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
  30. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
  31. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/session-remediation.d.ts +149 -0
  34. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
  35. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/time-format.d.ts +28 -0
  37. package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/types/entities.d.ts +8 -0
  39. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/types/requests-results.d.ts +6 -0
  41. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  42. package/packages/agent-team/preset/team-member/agent.cordis.yml +19 -4
  43. package/packages/client-agent-team/lib/client.js +80 -54
  44. package/packages/client-agent-team/lib/client.js.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamMessage.js +2 -2
  46. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +4 -4
  47. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  48. package/packages/client-agent-team/lib/types/client/index.js +10 -5
  49. package/packages/client-agent-team/lib/types/client/slots.d.ts +1 -1
  50. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  51. package/packages/tool-agent-team/README.md +8 -6
  52. package/packages/tool-agent-team/README.zh.md +8 -6
  53. package/packages/tool-agent-team/lib/index.js +302 -110
  54. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -39,58 +39,6 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
39
39
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
40
40
  done = true;
41
41
  };
42
- var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
43
- if (value !== null && value !== void 0) {
44
- if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
45
- var dispose, inner;
46
- if (async) {
47
- if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
48
- dispose = value[Symbol.asyncDispose];
49
- }
50
- if (dispose === void 0) {
51
- if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
52
- dispose = value[Symbol.dispose];
53
- if (async) inner = dispose;
54
- }
55
- if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
56
- if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
57
- env.stack.push({ value: value, dispose: dispose, async: async });
58
- }
59
- else if (async) {
60
- env.stack.push({ async: true });
61
- }
62
- return value;
63
- };
64
- var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
65
- return function (env) {
66
- function fail(e) {
67
- env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
68
- env.hasError = true;
69
- }
70
- var r, s = 0;
71
- function next() {
72
- while (r = env.stack.pop()) {
73
- try {
74
- if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
75
- if (r.dispose) {
76
- var result = r.dispose.call(r.value);
77
- if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
78
- }
79
- else s |= 1;
80
- }
81
- catch (e) {
82
- fail(e);
83
- }
84
- }
85
- if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
86
- if (env.hasError) throw env.error;
87
- }
88
- return next();
89
- };
90
- })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
91
- var e = new Error(message);
92
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
93
- });
94
42
  import { randomUUID } from 'node:crypto';
95
43
  import { isDeepStrictEqual } from 'node:util';
96
44
  import { Service } from '@deepseek-ai/cordis';
@@ -98,25 +46,27 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
98
46
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
99
47
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
100
48
  import { scopeOf } from '@deepseek-ai/dsh-scope';
101
- import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
49
+ import { Session, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
50
+ import { SessionFormatUnsupportedError } from '@deepseek-ai/dsh-session-persistence';
102
51
  import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
103
52
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
104
53
  import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
105
54
  import { PressurePolicyCoordinator } from "./pressure-policy.js";
106
55
  import { ContextManagementCoordinator } from "./context-management.js";
107
- import { createHandoffMessage } from "./context-source.js";
56
+ import { AGENT_TEAM_PLUGIN_ID, createHandoffMessage } from "./context-source.js";
108
57
  import { carriedInputOf, checkpointByRef, checkpointRefFor, foldContextProjection, isReminderNoticeSummary, timelineCandidates } from "./context-projection.js";
109
58
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
110
59
  import { AGENT_TEAM_TOOL_NAMES, deepCopyCapabilities, memberMemoryDirectoryName, MemberRuntime } from "./member-runtime.js";
111
60
  import { ProgressNudgeCoordinator } from "./progress-nudge.js";
112
61
  import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
62
+ import { SessionRemediation, handoffAlreadyInLog } from "./session-remediation.js";
113
63
  import { agentTeamDomainSpec } from "./spec.js";
64
+ import { formatTeamTimestamp } from "./time-format.js";
114
65
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
115
66
  export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
116
67
  export { AGENT_TEAM_TOOL_NAMES } from "./member-runtime.js";
117
68
  /** Process-stable marker carried by the final Team message tool definition. */
118
69
  export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.preset');
119
- const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
120
70
  const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
121
71
  const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
122
72
  const ORPHANED_MEMBER_DIAGNOSTIC = 'Member preset composition was lost after a reload; its tools are unavailable. Resume rebuilds the member in place.';
@@ -152,6 +102,35 @@ export function markAgentTeamPreset(definition) {
152
102
  Object.defineProperty(definition, AGENT_TEAM_PRESET_MARKER, { value: true });
153
103
  return definition;
154
104
  }
105
+ /**
106
+ * Whether the running dsh loads workspace packages from source via tsx.
107
+ *
108
+ * The Harness `tsconfig.base.json` maps `@deepseek-ai/*` package names onto
109
+ * `src/` directories; tsx honors those paths, so a CLI launched with
110
+ * `node --import tsx/esm apps/cli/src/bin.ts` imports `@deepseek-ai/dsh-scope`
111
+ * from `src/index.ts` while a profile-installed bundle resolves the compiled
112
+ * `lib/index.js` — two module instances with independent scope keys.
113
+ */
114
+ export function isTsxDevMode() {
115
+ const flags = [...process.execArgv];
116
+ const nodeOptions = process.env.NODE_OPTIONS;
117
+ if (nodeOptions !== undefined)
118
+ flags.push(...nodeOptions.split(/\s+/));
119
+ return flags.some(flag => flag.includes('tsx'));
120
+ }
121
+ /**
122
+ * The activation diagnostic for a dsh-scope module-instance mismatch.
123
+ *
124
+ * `agentPresets.mount` already rejected an unscoped context, so a scope key
125
+ * the harness sees but this bundle does not can only mean the two sides
126
+ * loaded different physical copies of `@deepseek-ai/dsh-scope`.
127
+ */
128
+ export function teamPresetScopeMismatchMessage(tsxDevMode) {
129
+ if (tsxDevMode) {
130
+ return 'selected preset is not team-enabled: the dsh CLI is running from source via tsx (tsconfig paths resolve @deepseek-ai/dsh-scope to src/), so the harness and this bundle load different module instances; start dsh with the compiled CLI instead (pnpm dsh, node apps/cli/lib/bin.js, or npx @deepseek-ai/dsh)';
131
+ }
132
+ return 'selected preset is not team-enabled: this bundle and the harness resolved different physical copies of @deepseek-ai/dsh-scope; run pnpm install in the profile directory so node_modules matches the lockfile, then restart';
133
+ }
155
134
  /**
156
135
  * A DM was durably recorded but its session injection could not run (no live
157
136
  * handle, or the wake itself failed). The recorded DM stays durable; the
@@ -454,9 +433,20 @@ let AgentTeam = (() => {
454
433
  if (initialization.committed)
455
434
  this.emitCommitted(initialization.value);
456
435
  this.startAttachmentGc(ledger);
436
+ // Legacy-artifact remediation runs before any Member activation: no write
437
+ // lease exists yet, so publishing sibling generations for refused Session
438
+ // logs cannot race a live writer. Remediation failure never blocks
439
+ // startup — the next start retries exactly what the cache does not cover.
440
+ try {
441
+ const remediation = new SessionRemediation(this.ctx, this.ctx.sessionPersistence, await SessionRemediation.open(this.ctx));
442
+ await remediation.remediateEnabledMembers(ledger.listMembers());
443
+ }
444
+ catch (error) {
445
+ this.ctx.logger.warn(`agent-team: legacy Session remediation did not run to completion (it will retry on the next start): ${error instanceof Error ? error.message : String(error)}`);
446
+ }
457
447
  // One metadata listing serves every Member restore; per-member list calls
458
448
  // would repeat the same I/O linearly during startup.
459
- const persistedSessions = new Set((await this.persistedSessionHeaders()).map(header => header.id));
449
+ const persistedSessions = new Set((await this.persistedSessionHeaders()).map(snapshot => snapshot.header.id));
460
450
  for (const member of ledger.listMembers()) {
461
451
  if (member.state === 'enabled')
462
452
  await this.activateMember(member, undefined, persistedSessions);
@@ -466,20 +456,14 @@ let AgentTeam = (() => {
466
456
  }
467
457
  /**
468
458
  * Whether one Member Session has durable persisted content, decided through
469
- * {@link SessionPersistenceService.inspect} rather than a bare metadata
470
- * listing: inspection first awaits any in-flight retirement drain for the
471
- * id, so a resume racing a suspend's fire-and-forget final flush cannot
472
- * mistake a still-draining persisted Session for an unpersisted one and
473
- * fork a fresh generation over it.
459
+ * {@link SessionPersistence.stat} rather than a bare metadata listing: the
460
+ * backend reports a still-draining session through its pending header, so a
461
+ * resume racing a suspend's fire-and-forget final flush cannot mistake a
462
+ * still-draining persisted Session for an unpersisted one and fork a fresh
463
+ * generation over it.
474
464
  */
475
465
  async sessionPersisted(sessionId) {
476
- try {
477
- await this.ctx.sessionPersistence.inspect(sessionId);
478
- return true;
479
- }
480
- catch {
481
- return false;
482
- }
466
+ return (await this.ctx.sessionPersistence.stat(sessionId)) !== undefined;
483
467
  }
484
468
  /** Resolve one exact live Agent to its durable Team Member; forks do not inherit identity. */
485
469
  memberForAgent(agent) {
@@ -1240,7 +1224,7 @@ let AgentTeam = (() => {
1240
1224
  }
1241
1225
  try {
1242
1226
  const message = createUserMessage({
1243
- content: [{ type: 'text', text: this.dmRelayText(agent, recipient, request.body.trim(), result.value.receipt.operationId) }],
1227
+ content: [{ type: 'text', text: this.dmRelayText(agent, recipient, request.body.trim(), result.value.receipt.occurredAt, result.value.receipt.operationId) }],
1244
1228
  source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'relay' },
1245
1229
  });
1246
1230
  // An idle recipient gets one ordinary turn; a busy one is steered into
@@ -1256,10 +1240,10 @@ let AgentTeam = (() => {
1256
1240
  return result.value;
1257
1241
  }
1258
1242
  /** Relay body: the DM itself plus one bounded line of adjacent context. */
1259
- dmRelayText(senderAgent, recipient, body, excluding) {
1243
+ dmRelayText(senderAgent, recipient, body, occurredAt, excluding) {
1260
1244
  const sender = this.memberForAgent(senderAgent);
1261
1245
  const prior = this.requireLedger().dmHistoryBetween(senderAgent.id, recipient.memberId, excluding);
1262
- const header = `Direct message from @${sender?.handle ?? 'a Team Member'}:`;
1246
+ const header = `Direct message from @${sender?.handle ?? 'a Team Member'} at ${formatTeamTimestamp(occurredAt)}:`;
1263
1247
  const context = prior === undefined ? '' : `\n\n[most recent prior DM between you: ${prior}]`;
1264
1248
  return `${header}\n\n${body}${context}`;
1265
1249
  }
@@ -1379,10 +1363,15 @@ let AgentTeam = (() => {
1379
1363
  }
1380
1364
  else {
1381
1365
  try {
1382
- const inspection = await this.ctx.sessionPersistence.inspect(sessionId);
1383
- events = inspection.events;
1384
- inheritedEventCount = inspection.inheritedEventCount;
1385
- parentSession = inspection.meta.parentSession;
1366
+ const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
1367
+ try {
1368
+ events = (await handle.read()).events;
1369
+ inheritedEventCount = handle.inheritedEventCount;
1370
+ parentSession = handle.header.parentSession;
1371
+ }
1372
+ finally {
1373
+ await handle.close();
1374
+ }
1386
1375
  }
1387
1376
  catch (error) {
1388
1377
  throw new Error(`checkpoint '${checkpointRef}' could not be resolved: its source Session is unreadable (${error instanceof Error ? error.message : String(error)})`);
@@ -1465,8 +1454,13 @@ let AgentTeam = (() => {
1465
1454
  return;
1466
1455
  let inspection;
1467
1456
  try {
1468
- const result = await this.ctx.sessionPersistence.inspect(previousSessionId);
1469
- inspection = result;
1457
+ const handle = await this.ctx.sessionPersistence.open(previousSessionId, 'read');
1458
+ try {
1459
+ inspection = { events: (await handle.read()).events, inheritedEventCount: handle.inheritedEventCount };
1460
+ }
1461
+ finally {
1462
+ await handle.close();
1463
+ }
1470
1464
  }
1471
1465
  catch (error) {
1472
1466
  this.ctx.logger.warn(`agent-team: rollover handoff reconstruction could not read the previous Session '${previousSessionId}': ${error instanceof Error ? error.message : String(error)}`);
@@ -1502,7 +1496,13 @@ let AgentTeam = (() => {
1502
1496
  async recordedCheckpointPrefix(seed) {
1503
1497
  let inspection;
1504
1498
  try {
1505
- inspection = await this.ctx.sessionPersistence.inspect(seed.sourceSessionId);
1499
+ const handle = await this.ctx.sessionPersistence.open(seed.sourceSessionId, 'read');
1500
+ try {
1501
+ inspection = { events: (await handle.read()).events, inheritedEventCount: handle.inheritedEventCount };
1502
+ }
1503
+ finally {
1504
+ await handle.close();
1505
+ }
1506
1506
  }
1507
1507
  catch (error) {
1508
1508
  throw new Error(`the recorded checkpoint-return seed source Session '${seed.sourceSessionId}' is unreadable: ${error instanceof Error ? error.message : String(error)}`);
@@ -1530,21 +1530,63 @@ let AgentTeam = (() => {
1530
1530
  * CURRENTLY present: delivered `user/message` ids plus the live inbox's
1531
1531
  * pending next-step/next-turn ids — a historical insert that was already
1532
1532
  * claimed (and removed) but never surfaced is NOT known and must replay.
1533
- * Fail-closed: an unreadable previous Session rejects the activation
1534
- * rather than silently dropping the Member's input.
1533
+ * Fail-closed for genuinely unreadable previous Sessions (missing/IO) so
1534
+ * the Member's input is never dropped silently, with three bounded
1535
+ * exceptions: a generation that already started its own turns needs no
1536
+ * replay (its carried input was delivered or superseded while it ran), a
1537
+ * log-corruption class error skips with a warning (the Host repairs torn
1538
+ * tails; a retired generation's corrupt log must not permanently block the
1539
+ * Member's activation), and a deterministic released-format refusal skips
1540
+ * with a warning (the candidate's own migration audit refuses that
1541
+ * artifact, so no retry can ever read it).
1535
1542
  */
1536
- async replayCarriedInput(member, agent) {
1543
+ async replayCarriedInput(member, agent, generationStarted) {
1537
1544
  const transition = this.requireLedger().lastTransitionForMember(member.memberId);
1538
1545
  if (transition === undefined || transition.targetSessionId !== agent.session.id)
1539
1546
  return 0;
1540
- let inspection;
1547
+ // A generation that already started its own turns needs no previous-Session
1548
+ // replay: its carried input was delivered with the handoff (or superseded)
1549
+ // before any own turn could run. Skipping the inspect also keeps a later
1550
+ // corruption or loss of the retired Session from re-blocking this Member
1551
+ // on every restart of the current generation.
1552
+ if (generationStarted) {
1553
+ this.ctx.logger.info(`agent-team: skipping carried-input replay for member '${this.memberLabel(member.memberId)}': the current Session '${agent.session.id}' already started a generation`);
1554
+ return 0;
1555
+ }
1556
+ let inspectionEvents;
1557
+ let inspectionInherited;
1541
1558
  try {
1542
- inspection = await this.ctx.sessionPersistence.inspect(transition.previousSessionId);
1559
+ const handle = await this.ctx.sessionPersistence.open(transition.previousSessionId, 'read');
1560
+ try {
1561
+ inspectionEvents = (await handle.read()).events;
1562
+ inspectionInherited = handle.inheritedEventCount;
1563
+ }
1564
+ finally {
1565
+ await handle.close();
1566
+ }
1543
1567
  }
1544
1568
  catch (error) {
1545
- throw new Error(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable: ${error instanceof Error ? error.message : String(error)}`);
1569
+ const detail = error instanceof Error ? error.message : String(error);
1570
+ // Log-corruption class: bounded fail-open. The current Session's own
1571
+ // fold is intact and the Host repairs the retired log's torn tail; a
1572
+ // corrupted retired generation must not permanently block activation.
1573
+ if (/corrupt session log/.test(detail)) {
1574
+ this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is corrupt: ${detail}; skipping the replay`);
1575
+ return 0;
1576
+ }
1577
+ // Deterministic released-format refusal: the retired artifact is refused
1578
+ // by the candidate's own migration audit, which no retry can change.
1579
+ // Failing activation here would turn a data problem in a Session the
1580
+ // Member no longer runs in into permanent unavailability — the exact
1581
+ // failure class this hardening removes. Genuine IO and unknown failures
1582
+ // stay fail-closed so carried input is never dropped silently.
1583
+ if (error instanceof SessionFormatUnsupportedError) {
1584
+ this.ctx.logger.warn(`agent-team: previous Session '${transition.previousSessionId}' holding carried input for member '${this.memberLabel(member.memberId)}' is refused by the session-format migration: ${detail}; skipping the replay`);
1585
+ return 0;
1586
+ }
1587
+ throw new Error(`the previous Session '${transition.previousSessionId}' holding the Member's carried input is unreadable: ${detail}`);
1546
1588
  }
1547
- const state = foldContextProjection(inspection.events, inspection.inheritedEventCount, transition.previousSessionId);
1589
+ const state = foldContextProjection(inspectionEvents, inspectionInherited, transition.previousSessionId);
1548
1590
  const carried = carriedInputOf(state);
1549
1591
  if (carried.length === 0)
1550
1592
  return 0;
@@ -1635,11 +1677,17 @@ let AgentTeam = (() => {
1635
1677
  }
1636
1678
  else {
1637
1679
  try {
1638
- const inspection = await this.ctx.sessionPersistence.inspect(sessionId);
1639
- state = foldContextProjection(inspection.events, inspection.inheritedEventCount, sessionId);
1640
- sourceEvents = inspection.events;
1641
- sourceSessionId = sessionId;
1642
- sessionId = inspection.meta.parentSession;
1680
+ const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
1681
+ try {
1682
+ const { events } = await handle.read();
1683
+ state = foldContextProjection(events, handle.inheritedEventCount, sessionId);
1684
+ sourceEvents = events;
1685
+ sourceSessionId = sessionId;
1686
+ sessionId = handle.header.parentSession;
1687
+ }
1688
+ finally {
1689
+ await handle.close();
1690
+ }
1643
1691
  }
1644
1692
  catch {
1645
1693
  // An unreadable ancestor ends the lineage walk here.
@@ -1682,29 +1730,17 @@ let AgentTeam = (() => {
1682
1730
  if (live)
1683
1731
  return meter.measure(agent.session)?.totalTokens;
1684
1732
  try {
1685
- const env_1 = { stack: [], error: void 0, hasError: false };
1733
+ // 0.1.5 removed borrowSession: rebuild a detached Session from the
1734
+ // stored log so the seed's retained cost is priced by the SOURCE's own
1735
+ // replay, never the current generation's.
1736
+ const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
1686
1737
  try {
1687
- const borrowed = __addDisposableResource(env_1, await this.ctx.sessionPersistence.borrowSession(sessionId)
1688
- // The borrow resolves the exact Session object for both shapes: the
1689
- // live instance for an attached Session, the prepared one for an
1690
- // archived ancestor. Measuring through the service context's store
1691
- // would need an inject this service does not declare.
1692
- , false);
1693
- // The borrow resolves the exact Session object for both shapes: the
1694
- // live instance for an attached Session, the prepared one for an
1695
- // archived ancestor. Measuring through the service context's store
1696
- // would need an inject this service does not declare.
1697
- const session = borrowed.source === 'live'
1698
- ? agent.session.id === sessionId ? agent.session : undefined
1699
- : borrowed.preparedSession;
1700
- return session === undefined ? undefined : meter.measure(session)?.totalTokens;
1701
- }
1702
- catch (e_1) {
1703
- env_1.error = e_1;
1704
- env_1.hasError = true;
1738
+ const { events } = await handle.read();
1739
+ const session = Session.create(sessionId, events, handle.header, handle.inheritedEventCount);
1740
+ return meter.measure(session)?.totalTokens;
1705
1741
  }
1706
1742
  finally {
1707
- __disposeResources(env_1);
1743
+ await handle.close();
1708
1744
  }
1709
1745
  }
1710
1746
  catch {
@@ -2060,7 +2096,7 @@ let AgentTeam = (() => {
2060
2096
  // allow-list filters the catalog by name through the live ref below.
2061
2097
  // `swap` is bound by the provider at activation (no-op until then).
2062
2098
  const skillSelection = { current: member.capabilities?.skills?.allow, swap: () => { } };
2063
- const setup = async (agentCtx) => {
2099
+ const setup = async (agentCtx, agent) => {
2064
2100
  await this.ctx.agentPresets.mount(agentCtx, member.presetId);
2065
2101
  this.memberRuntime.applyMemberToolPolicy(agentCtx, member);
2066
2102
  this.validateMemberPreset(agentCtx);
@@ -2107,9 +2143,6 @@ let AgentTeam = (() => {
2107
2143
  });
2108
2144
  return {
2109
2145
  commit: () => {
2110
- const agent = agentCtx.agent;
2111
- if (agent === undefined)
2112
- throw new Error('agent-team setup has no unpublished Agent');
2113
2146
  // The member scope composes no sandbox-policy service (the preset
2114
2147
  // owns no sandbox row), so read the last logged mode straight from
2115
2148
  // the session log; `sandbox/mode` is log-only and never joins the
@@ -2179,6 +2212,11 @@ let AgentTeam = (() => {
2179
2212
  // The one-shot pressure notice needs no explicit re-arm here: it latches
2180
2213
  // on durable Session evidence, and a fresh generation's own event span
2181
2214
  // starts empty — a new Session is itself the re-arm.
2215
+ // Snapshot whether this generation had already started its own turns
2216
+ // BEFORE any recovery delivery below: the handoff steer and the Inbox
2217
+ // wake legitimately append `turn/start` to this Session, and those must
2218
+ // never be mistaken for the generation having run on its own.
2219
+ const generationStarted = created.agent.session.ownEvents().some(event => event.type === 'turn/start');
2182
2220
  // A restart between a durable rollover intent and its swap replays the
2183
2221
  // old Session; the projection still carries the intent, so finish the
2184
2222
  // transition (or keep waiting for the containing turn) from here.
@@ -2194,14 +2232,18 @@ let AgentTeam = (() => {
2194
2232
  // delivered activates the new Session with no handoff in its own
2195
2233
  // log — never treat that as an ordinary blank Member Session. The
2196
2234
  // operation's recorded previous Session (the lineage parent) still
2197
- // holds the intent; rebuild the handoff from it.
2198
- if (!state.boundaries.some(boundary => boundary.source === 'handoff')) {
2235
+ // holds the intent; rebuild the handoff from it. "No handoff in its
2236
+ // own log" is judged over both shapes: a generation rescued from the
2237
+ // retired custom kinds carries its handoff as a source the projection
2238
+ // does not classify, and rebuilding on top of it would inject the same
2239
+ // handoff twice.
2240
+ if (!handoffAlreadyInLog(state.boundaries, created.agent.session.ownEvents())) {
2199
2241
  await this.reconstructMissingHandoff(member, created.agent);
2200
2242
  }
2201
2243
  // Carried input redelivery binds to the committed transition target —
2202
2244
  // this Session — never to the handoff's presence: a crash after the
2203
2245
  // handoff landed but before the carried enqueue still replays here.
2204
- recoveryCarried = await this.replayCarriedInput(member, created.agent);
2246
+ recoveryCarried = await this.replayCarriedInput(member, created.agent, generationStarted);
2205
2247
  }
2206
2248
  else if (forkedFrom === undefined && lineageParent !== undefined) {
2207
2249
  // A restart recreated a Session that never materialized before the
@@ -2215,7 +2257,7 @@ let AgentTeam = (() => {
2215
2257
  // handoff right after — that delivery is the plan, never this
2216
2258
  // recovery.
2217
2259
  await this.reconstructMissingHandoff(member, created.agent);
2218
- recoveryCarried = await this.replayCarriedInput(member, created.agent);
2260
+ recoveryCarried = await this.replayCarriedInput(member, created.agent, generationStarted);
2219
2261
  }
2220
2262
  // The ordinary Inbox wake runs LAST, after any recovery delivery above:
2221
2263
  // the handoff must stay the first model-facing context of a recovered
@@ -2232,7 +2274,9 @@ let AgentTeam = (() => {
2232
2274
  await created?.dispose();
2233
2275
  this.modelSelections.delete(member.memberId);
2234
2276
  this.memberRuntime.forgetMember(member.memberId);
2235
- this.setMemberFailure(member.memberId, 'activation', error instanceof Error ? error.message : String(error));
2277
+ const message = error instanceof Error ? error.message : String(error);
2278
+ this.ctx.logger.warn(`agent-team: activation failed for member '${this.memberLabel(member.memberId)}': ${message}`);
2279
+ this.setMemberFailure(member.memberId, 'activation', message);
2236
2280
  }
2237
2281
  finally {
2238
2282
  // Activation only changes this Workspace's presence projection.
@@ -2292,6 +2336,12 @@ let AgentTeam = (() => {
2292
2336
  const scope = scopeOf(agentCtx);
2293
2337
  const teamMessage = this.ctx.tools.get('team_message', scope);
2294
2338
  if (teamMessage?.[AGENT_TEAM_PRESET_MARKER] !== true) {
2339
+ // `agentPresets.mount` already rejected an unscoped context, so a scope
2340
+ // key the harness sees but this bundle does not means the two sides
2341
+ // loaded different module instances of @deepseek-ai/dsh-scope (the
2342
+ // common trigger is running the CLI from source via tsx).
2343
+ if (scope === undefined)
2344
+ throw new Error(teamPresetScopeMismatchMessage(isTsxDevMode()));
2295
2345
  throw new Error('selected preset is not team-enabled');
2296
2346
  }
2297
2347
  const available = new Set(this.ctx.tools.schemas(scope).map(tool => tool.name));
@@ -2503,7 +2553,7 @@ let AgentTeam = (() => {
2503
2553
  if (direct && fact.kind === 'message') {
2504
2554
  const sender = fact.message.sender === AGENT_TEAM_HUMAN_MEMBER_ID
2505
2555
  ? 'human' : this.requireLedger().getMember(fact.message.sender)?.handle ?? fact.message.sender;
2506
- const detail = ['Direct Team mention', `From: ${sender}`, `Channel: ${item.channelRef}`,
2556
+ const detail = ['Direct Team mention', `Occurred at: ${formatTeamTimestamp(fact.occurredAt)}`, `From: ${sender}`, `Channel: ${item.channelRef}`,
2507
2557
  ...(item.task === undefined ? [] : [`Task: ${item.task.taskRef}`]),
2508
2558
  `Thread: ${item.thread.threadRef}`, `Message ref: ${fact.message.messageRef}`,
2509
2559
  `Message: ${this.boundedNotificationBody(fact.message.body)}`].join('\n');
@@ -2511,15 +2561,15 @@ let AgentTeam = (() => {
2511
2561
  detailedFactCount += 1;
2512
2562
  }
2513
2563
  else if (fact.kind === 'activity') {
2514
- const detail = `${this.activityNotification(fact.activity, readerId)}\nThread: ${item.thread.threadRef}`;
2564
+ const detail = `${this.activityNotification(fact.activity, readerId)}\nOccurred at: ${formatTeamTimestamp(fact.occurredAt)}\nThread: ${item.thread.threadRef}`;
2515
2565
  if (append(detail))
2516
2566
  detailedFactCount += 1;
2517
2567
  }
2518
2568
  }
2519
- const ordinaryCount = facts.filter(entry => entry.fact.kind === 'message' && !entry.direct).length;
2520
- if (ordinaryCount > 0) {
2569
+ const ordinary = facts.filter(entry => entry.fact.kind === 'message' && !entry.direct);
2570
+ if (ordinary.length > 0) {
2521
2571
  const route = item.task === undefined ? `Thread ${item.thread.threadRef}` : `Task ${item.task.taskRef}`;
2522
- append(`${route}: ${ordinaryCount} unread update${ordinaryCount === 1 ? '' : 's'}.`);
2572
+ append(`${route}: ${ordinary.length} unread update${ordinary.length === 1 ? '' : 's'} · newest at ${formatTeamTimestamp(ordinary.at(-1).fact.occurredAt)}.`);
2523
2573
  }
2524
2574
  }
2525
2575
  if (omitted)