@ycodium-ai/plugin-team 0.2.3250 → 0.2.3254

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.
package/dist/index.js CHANGED
@@ -1182,8 +1182,8 @@ var succeed = (success) => {
1182
1182
  };
1183
1183
 
1184
1184
  // ../../node_modules/.pnpm/effect@4.0.0-beta.103_patch_hash=78523ded7c3dfc3efd3cec0247f4960d00eaa3d3c96a2077457694a4d8b453c6/node_modules/effect/dist/Order.js
1185
- function make(compare2) {
1186
- return (self, that) => self === that ? 0 : compare2(self, that);
1185
+ function make(compare3) {
1186
+ return (self, that) => self === that ? 0 : compare3(self, that);
1187
1187
  }
1188
1188
  var Number2 = /* @__PURE__ */ make((self, that) => {
1189
1189
  if (globalThis.Number.isNaN(self) && globalThis.Number.isNaN(that)) return 0;
@@ -7205,12 +7205,13 @@ function leadBrief(goal, roster) {
7205
7205
  LEAD_OWN_ITEM_RULE,
7206
7206
  LEAD_SEND_BACK_RULE,
7207
7207
  LEAD_STUCK_RULE,
7208
- `Only after all dependencies are recorded, assign each item to exactly one member with work_update_item. For local members use assignees: [{ "kind": "agent", "threadId": "<threadId from the table>" }]. A local member is told about an item as soon as it is assigned, so assigning before the dependencies exist starts blocked work early. ${LEAD_NO_MEMBER_TASKS}`,
7208
+ `Only after all dependencies are recorded, assign each item to exactly one member with work_update_item. For local members use assignees: [{ "kind": "agent", "threadId": "<threadId from the table>" }]. Assign work to members with no item in progress first; do not give one member two items while another is idle. An item assigned to a local member who already has work waits in line and wakes them after their current item is finished. Record dependencies before assigning so blocked work waits until it can start. ${LEAD_NO_MEMBER_TASKS}`,
7209
7209
  ...remoteMembers.length === 0 ? [] : [
7210
7210
  'Remote members are on another machine. To give them work, assign the work item to them with work_update_item and assignees: [{ "kind": "remote-agent", "principalId": "<principalId from the table>" }]; they pick it up from their own machine. Do not create conversations, start turns, or send wake-up signals for them on this machine.',
7211
7211
  "A comment that mentions a remote member, or replies to one of their comments, wakes them on their machine, so you can ask or answer them in work item comments. Once they mark an item done they stop working on it, so to send work back to a remote member you still reopen the item as described above."
7212
7212
  ],
7213
7213
  "Use work_list_items to check progress. When all work is complete, summarize the results here in one or two sentences.",
7214
+ "Members' worktrees and branches remain after the goal ends. After their work is merged into the main line, they can be deleted from the source control panel.",
7214
7215
  PERSON_MESSAGE_RULE,
7215
7216
  "If the work item tools are unavailable, tell the user directly that this project needs the board and project memory switches turned on first."
7216
7217
  ].join("\n\n");
@@ -7218,6 +7219,7 @@ function leadBrief(goal, roster) {
7218
7219
 
7219
7220
  // src/domain/goals.ts
7220
7221
  var REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
7222
+ var isTeamRequestId = (requestId) => REQUEST_ID_PATTERN.test(requestId);
7221
7223
  var GOAL_MAX_LENGTH = 4e3;
7222
7224
  var goalText = (raw) => {
7223
7225
  const text = raw.trim();
@@ -7333,7 +7335,7 @@ function createGoalService(ctx, rosterService, options = {}) {
7333
7335
  const input = decoded.value;
7334
7336
  const text = goalText(input.goal);
7335
7337
  if (text === null) return rejectGoal("invalid-goal");
7336
- if (!REQUEST_ID_PATTERN.test(input.requestId)) return rejectGoal("invalid-request-id");
7338
+ if (!isTeamRequestId(input.requestId)) return rejectGoal("invalid-request-id");
7337
7339
  const roster = await rosterService.read(input.projectId);
7338
7340
  if (roster === null) return rejectGoal("no-roster");
7339
7341
  const project2 = await ctx.projects.get({ projectId: input.projectId });
@@ -8189,9 +8191,74 @@ function createRosterService(ctx, options = {}) {
8189
8191
  };
8190
8192
  }
8191
8193
 
8194
+ // src/runtime/wakeQueries.ts
8195
+ function memberWorktreeFor(goals, project2, projectId, recipient) {
8196
+ if (!("threadId" in recipient) || project2 === void 0 || !project2.threads.includes(recipient.threadId) || project2.leadThreads?.includes(recipient.threadId))
8197
+ return;
8198
+ const { threadId } = recipient;
8199
+ const leadThreadId = project2.leadThreads?.[0];
8200
+ if (leadThreadId === void 0) return;
8201
+ return {
8202
+ leadThreadId,
8203
+ failure: project2.worktreeFailures?.[threadId],
8204
+ // 只在要建树或报失败时读目标取名字,叫醒的热路径不多读。
8205
+ name: async () => (await goals.read(projectId))?.members.find((member) => member.threadId === threadId)?.name ?? project2.memberIds?.[threadId] ?? threadId
8206
+ };
8207
+ }
8208
+ var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
8209
+ var compareWakeItems = (a, b) => a.sequenceId - b.sequenceId || compare(a.workItemId, b.workItemId);
8210
+ var isClosed = (item) => item?.stateGroup === "completed" || item?.stateGroup === "cancelled";
8211
+ var hasOpenBlocker = (project2, workItemId) => (readFact(project2.blockers, workItemId) ?? []).some(
8212
+ (blocker) => !isClosed(readFact(project2.items, blocker))
8213
+ );
8214
+ var isAssignedOpen = (project2, workItemId, threadId) => {
8215
+ const item = readFact(project2.items, workItemId);
8216
+ return item !== null && item !== void 0 && !isClosed(item) && item.assigneeThreads.includes(threadId);
8217
+ };
8218
+ var isWorkable = (project2, workItemId) => !isClosed(readFact(project2.items, workItemId)) && !hasOpenBlocker(project2, workItemId);
8219
+ var isOpenWorkFor = (project2, workItemId, threadId) => isAssignedOpen(project2, workItemId, threadId) && !hasOpenBlocker(project2, workItemId);
8220
+ var isMember = (project2, recipient) => "threadId" in recipient ? project2.threads.includes(recipient.threadId) : project2.remoteMembers.includes(recipient.principalId);
8221
+ var isQueuedUnclaimed = (item, threadId) => item.wakeStatus?.[threadId] === "pending" && !item.claimedBy.includes(threadId);
8222
+ var isDeferredWake = (project2, item, threadId) => project2.threads.includes(threadId) && !project2.leadThreads?.includes(threadId) && item.assigneeThreads.includes(threadId) && isQueuedUnclaimed(item, threadId) && !isClosed(item);
8223
+ function countDeferredWakes(project2, threadId) {
8224
+ let count = 0;
8225
+ for (const workItemId in project2.items) {
8226
+ const item = readFact(project2.items, workItemId);
8227
+ if (item !== null && isDeferredWake(project2, item, threadId)) count++;
8228
+ }
8229
+ return count;
8230
+ }
8231
+
8192
8232
  // src/runtime/deliver.ts
8193
8233
  import * as NodeCrypto from "node:crypto";
8194
8234
  var stableHash = (key) => NodeCrypto.createHash("sha256").update(key).digest("hex").slice(0, 32);
8235
+ var memberWorktreeBranch = (name, threadId) => {
8236
+ const slug = Array.from(
8237
+ name.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-")
8238
+ ).slice(0, 32).join("").replace(/^-+|-+$/g, "") || "member";
8239
+ return `team/${slug}-${stableHash(threadId).slice(0, 16)}`;
8240
+ };
8241
+ var createMemberWorktreeLane = (state, events) => {
8242
+ const tails = /* @__PURE__ */ new Map();
8243
+ return {
8244
+ state,
8245
+ events,
8246
+ serialize: (threadId, run3) => {
8247
+ const next = (tails.get(threadId) ?? Promise.resolve()).then(run3);
8248
+ const tail = next.then(
8249
+ () => void 0,
8250
+ () => void 0
8251
+ );
8252
+ tails.set(threadId, tail);
8253
+ void tail.then(() => {
8254
+ if (tails.get(threadId) === tail) tails.delete(threadId);
8255
+ });
8256
+ return next;
8257
+ }
8258
+ };
8259
+ };
8260
+ var shortReason = (error) => (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim().slice(0, 240) || "Worktree preparation failed";
8261
+ var FALLBACK_SUFFIX = ":worktree-fallback";
8195
8262
  async function deliverWake(ctx, input) {
8196
8263
  const { recipient } = input;
8197
8264
  if ("principalId" in recipient) {
@@ -8205,26 +8272,406 @@ async function deliverWake(ctx, input) {
8205
8272
  } else {
8206
8273
  const execution = await ctx.projections.getThreadExecution(recipient.threadId);
8207
8274
  if (execution === null || execution.archivedAt !== null) return false;
8208
- await ctx.threads.turns.start({
8209
- requestId: input.requestId,
8210
- threadId: recipient.threadId,
8275
+ const { threadId } = recipient;
8276
+ const text = typeof input.text === "string" ? input.text : await input.text();
8277
+ const worktree = input.memberWorktree;
8278
+ const turn = {
8279
+ threadId,
8211
8280
  message: {
8212
8281
  messageId: input.messageId,
8213
8282
  role: "user",
8214
- text: typeof input.text === "string" ? input.text : await input.text(),
8283
+ text,
8215
8284
  attachments: []
8216
8285
  },
8217
8286
  modelSelection: execution.modelSelection,
8218
8287
  runtimeMode: execution.runtimeMode,
8219
8288
  interactionMode: execution.interactionMode
8289
+ };
8290
+ const startTurn = (suffix) => ctx.threads.turns.start({
8291
+ ...turn,
8292
+ requestId: `${input.requestId}${suffix}`,
8293
+ message: { ...turn.message, messageId: `${input.messageId}${suffix}` }
8220
8294
  });
8295
+ let failure = worktree?.failure;
8296
+ let delivered = false;
8297
+ if (worktree !== void 0 && failure === void 0 && !execution.worktreePath) {
8298
+ const { lane } = worktree;
8299
+ const first = await lane.serialize(threadId, async () => {
8300
+ const current = await ctx.projections.getThreadExecution(threadId);
8301
+ if (current?.worktreePath) return { kind: "registered" };
8302
+ const recorded = (await lane.state.get())[input.projectId]?.worktreeFailures?.[threadId];
8303
+ if (recorded !== void 0) return { kind: "failed", failure: recorded };
8304
+ const branch = memberWorktreeBranch(await worktree.name(), threadId);
8305
+ try {
8306
+ await ctx.threads.turns.start({
8307
+ ...turn,
8308
+ requestId: input.requestId,
8309
+ message: {
8310
+ ...turn.message,
8311
+ text: `${text}
8312
+
8313
+ You are in your own newly created worktree on branch ${branch}. Install dependencies according to the project's instructions before starting work.`
8314
+ },
8315
+ bootstrap: {
8316
+ prepareWorktree: {
8317
+ // bootstrapTurnStart 将它传作 refName;gitDriver/worktrees 用 cwd 下的 HEAD 执行 git worktree add -b。
8318
+ baseBranch: "HEAD",
8319
+ branch,
8320
+ startFromOrigin: false
8321
+ }
8322
+ }
8323
+ });
8324
+ } catch (error) {
8325
+ await startTurn(FALLBACK_SUFFIX);
8326
+ const reason = shortReason(error);
8327
+ const receipt = await lane.events.append({
8328
+ commandId: `team-worktree-failed:${threadId}`,
8329
+ name: "member-worktree-failed",
8330
+ aggregate: { kind: "project", id: input.projectId },
8331
+ data: { version: 1, projectId: input.projectId, threadId, reason }
8332
+ });
8333
+ const persisted = (await lane.state.get({ throughSequence: receipt.sequence }))[input.projectId]?.worktreeFailures?.[threadId];
8334
+ return { kind: "fallback", failure: persisted ?? { reason } };
8335
+ }
8336
+ return { kind: "worktree" };
8337
+ });
8338
+ if (first.kind === "worktree") return true;
8339
+ if (first.kind !== "registered") failure = first.failure;
8340
+ delivered = first.kind === "fallback";
8341
+ }
8342
+ if (!delivered) await startTurn(failure === void 0 ? "" : FALLBACK_SUFFIX);
8343
+ if (worktree !== void 0 && failure !== void 0 && failure.reported !== true) {
8344
+ const reported = await deliverWake(ctx, {
8345
+ projectId: input.projectId,
8346
+ recipient: { threadId: worktree.leadThreadId },
8347
+ requestId: `team-worktree-report:${threadId}`,
8348
+ messageId: `team-worktree-report-msg:${threadId}`,
8349
+ text: `Could not create a worktree for member ${await worktree.name()} (threadId ${threadId}): ${failure.reason}. They will work in the project folder for now.`
8350
+ });
8351
+ if (!reported) throw new Error("Member worktree failure could not be reported to the lead");
8352
+ const receipt = await worktree.lane.events.append({
8353
+ commandId: `team-worktree-reported:${threadId}`,
8354
+ name: "member-worktree-reported",
8355
+ aggregate: { kind: "project", id: input.projectId },
8356
+ data: { version: 1, projectId: input.projectId, threadId }
8357
+ });
8358
+ await worktree.lane.state.get({ throughSequence: receipt.sequence });
8359
+ }
8221
8360
  }
8222
8361
  return true;
8223
8362
  }
8224
8363
 
8364
+ // src/runtime/recoveryState.ts
8365
+ var RecoveryPlan = Struct({
8366
+ action: Literals(["resume", "lead", "remind", "stall-lead"]),
8367
+ recipientThreadId: String4,
8368
+ text: String4,
8369
+ workItemIds: ArraySchema(String4),
8370
+ resumeQueue: optionalKey2(Literal2(true))
8371
+ });
8372
+ var REQUESTED = "team/member-recovery-requested";
8373
+ var DELIVERED = "team/member-recovery-delivered";
8374
+ var decodeRecoveryFact = decodeUnknownOption2(
8375
+ Struct({
8376
+ pluginId: Literal2("team"),
8377
+ name: Literals([REQUESTED, DELIVERED])
8378
+ })
8379
+ );
8380
+ var isRecoveryFact = (event) => event.type === "plugin.event-appended" && isSome2(decodeRecoveryFact(event.payload));
8381
+ var decodeGoal = decodeUnknownOption2(
8382
+ Struct({
8383
+ pluginId: Literal2("team"),
8384
+ name: Literal2("team/goal-started"),
8385
+ data: GoalStartedFact
8386
+ })
8387
+ );
8388
+ var decodeDelivered = decodeUnknownOption2(
8389
+ Struct({
8390
+ pluginId: Literal2("team"),
8391
+ name: Literal2(DELIVERED),
8392
+ data: Struct({
8393
+ version: Literal2(1),
8394
+ projectId: String4,
8395
+ threadId: String4,
8396
+ turnId: String4,
8397
+ failureSequence: Number5,
8398
+ action: RecoveryPlan.fields.action,
8399
+ workItemIds: ArraySchema(String4)
8400
+ })
8401
+ })
8402
+ );
8403
+ var decodeRequested = decodeUnknownOption2(
8404
+ Struct({
8405
+ pluginId: Literal2("team"),
8406
+ name: Literal2(REQUESTED),
8407
+ data: Struct({
8408
+ version: Literal2(1),
8409
+ projectId: String4,
8410
+ threadId: String4,
8411
+ turnId: String4,
8412
+ failureSequence: Number5,
8413
+ plan: RecoveryPlan
8414
+ })
8415
+ })
8416
+ );
8417
+ var decodeSession = decodeUnknownOption2(
8418
+ Struct({
8419
+ threadId: String4,
8420
+ session: Struct({
8421
+ status: String4,
8422
+ activeTurnId: NullOr(String4),
8423
+ failureCategory: optionalKey2(String4)
8424
+ })
8425
+ })
8426
+ );
8427
+ var decodeWork = decodeUnknownOption2(
8428
+ Struct({
8429
+ projectId: String4,
8430
+ workItemId: String4,
8431
+ actor: optionalKey2(
8432
+ Struct({ kind: String4, threadId: optionalKey2(String4) })
8433
+ )
8434
+ })
8435
+ );
8436
+ function recoveryEventScopeOf(event) {
8437
+ if (event.aggregateKind === "project") return { kind: "project", projectId: event.aggregateId };
8438
+ if (event.aggregateKind === "thread") return { kind: "thread", threadId: event.aggregateId };
8439
+ }
8440
+ function foldRecovery(state, event, previous) {
8441
+ if (event.type === "plugin.event-appended") {
8442
+ const goalFact = decodeGoal(event.payload);
8443
+ if (isSome2(goalFact)) {
8444
+ const { goal } = goalFact.value.data;
8445
+ const project2 = readFact(state, goal.projectId);
8446
+ if (project2 === null) return state;
8447
+ const recovery = {};
8448
+ for (const threadId of project2.threads) {
8449
+ if ((project2.leadThreads ?? []).includes(threadId)) continue;
8450
+ recovery[threadId] = {
8451
+ turnId: null,
8452
+ status: null,
8453
+ retriedItemIds: [],
8454
+ handledTurnIds: [],
8455
+ pending: null
8456
+ };
8457
+ }
8458
+ return { ...state, [goal.projectId]: { ...project2, recovery } };
8459
+ }
8460
+ const requested = decodeRequested(event.payload);
8461
+ if (isSome2(requested)) {
8462
+ const fact = requested.value.data;
8463
+ const project2 = readFact(state, fact.projectId);
8464
+ const member = project2?.recovery?.[fact.threadId];
8465
+ if (project2 === null || member === void 0 || member.pending?.turnId !== fact.turnId)
8466
+ return state;
8467
+ const retriedItemIds = fact.plan.action === "resume" ? [.../* @__PURE__ */ new Set([...member.retriedItemIds, ...fact.plan.workItemIds])].filter(
8468
+ (id) => isAssignedOpen(project2, id, fact.threadId)
8469
+ ) : member.retriedItemIds;
8470
+ return {
8471
+ ...state,
8472
+ [fact.projectId]: {
8473
+ ...project2,
8474
+ recovery: {
8475
+ ...project2.recovery,
8476
+ [fact.threadId]: {
8477
+ ...member,
8478
+ retriedItemIds,
8479
+ ...fact.plan.action === "resume" ? {
8480
+ unprogressedRetryItemIds: [
8481
+ .../* @__PURE__ */ new Set([
8482
+ ...member.unprogressedRetryItemIds ?? [],
8483
+ ...fact.plan.workItemIds
8484
+ ])
8485
+ ]
8486
+ } : {},
8487
+ ...fact.plan.action === "remind" ? {
8488
+ remindedItemIds: [
8489
+ .../* @__PURE__ */ new Set([...member.remindedItemIds ?? [], ...fact.plan.workItemIds])
8490
+ ]
8491
+ } : {},
8492
+ ...fact.plan.action === "stall-lead" ? {
8493
+ escalatedItemIds: [
8494
+ .../* @__PURE__ */ new Set([...member.escalatedItemIds ?? [], ...fact.plan.workItemIds])
8495
+ ]
8496
+ } : {},
8497
+ pending: { ...member.pending, plan: fact.plan }
8498
+ }
8499
+ }
8500
+ }
8501
+ };
8502
+ }
8503
+ const delivered = decodeDelivered(event.payload);
8504
+ if (isSome2(delivered)) {
8505
+ const fact = delivered.value.data;
8506
+ const project2 = readFact(state, fact.projectId);
8507
+ const member = project2?.recovery?.[fact.threadId];
8508
+ if (project2 === null || member === void 0 || member.handledTurnIds.includes(fact.turnId))
8509
+ return state;
8510
+ return {
8511
+ ...state,
8512
+ [fact.projectId]: {
8513
+ ...project2,
8514
+ recovery: {
8515
+ ...project2.recovery,
8516
+ [fact.threadId]: {
8517
+ ...member,
8518
+ handledTurnIds: [...member.handledTurnIds, fact.turnId],
8519
+ pending: member.pending?.turnId === fact.turnId ? null : member.pending
8520
+ }
8521
+ }
8522
+ }
8523
+ };
8524
+ }
8525
+ }
8526
+ if (event.type === "thread.session-set") {
8527
+ const decoded = decodeSession(event.payload);
8528
+ if (isNone2(decoded)) return state;
8529
+ const { threadId, session } = decoded.value;
8530
+ for (const [projectId, project2] of Object.entries(state)) {
8531
+ const member = project2.recovery?.[threadId];
8532
+ if (member === void 0 || !isMember(project2, { threadId })) continue;
8533
+ const turnId = session.activeTurnId ?? member.turnId;
8534
+ if (session.status !== "running" && turnId !== null && member.handledTurnIds.includes(turnId))
8535
+ return state;
8536
+ const succeeded = (session.status === "ready" || session.status === "idle") && member.status === "running";
8537
+ const failed = session.status === "error" && turnId !== null;
8538
+ if (member.status === session.status && member.turnId === turnId && !failed) return state;
8539
+ const hasStarted = Object.values(project2.items).some(
8540
+ (item) => item.stateGroup === "started" && item.assigneeThreads.includes(threadId)
8541
+ );
8542
+ const hasOpenWork = Object.keys(project2.items).some(
8543
+ (id) => isOpenWorkFor(project2, id, threadId)
8544
+ );
8545
+ const queuedContinuation = member.pending?.plan?.resumeQueue === true && !failed;
8546
+ const pending = queuedContinuation ? member.pending : failed && hasOpenWork ? member.pending?.turnId === turnId && member.pending.kind !== "stall" ? { ...member.pending, category: session.failureCategory ?? member.pending.category } : { turnId, category: session.failureCategory ?? "unknown", sequence: event.sequence } : succeeded && turnId !== null && hasStarted ? {
8547
+ turnId,
8548
+ category: "normal-end",
8549
+ kind: "stall",
8550
+ sequence: event.sequence,
8551
+ continuedItemIds: member.unprogressedRetryItemIds ?? []
8552
+ } : session.status === "running" || succeeded ? null : member.pending;
8553
+ return {
8554
+ ...state,
8555
+ [projectId]: {
8556
+ ...project2,
8557
+ recovery: {
8558
+ ...project2.recovery,
8559
+ [threadId]: {
8560
+ ...member,
8561
+ turnId,
8562
+ status: session.status,
8563
+ retriedItemIds: succeeded ? [] : member.retriedItemIds,
8564
+ ...succeeded ? { unprogressedRetryItemIds: [] } : {},
8565
+ handledTurnIds: failed && !hasOpenWork ? [...member.handledTurnIds, turnId] : session.status === "running" && member.pending?.plan !== void 0 && !queuedContinuation ? [.../* @__PURE__ */ new Set([...member.handledTurnIds, member.pending.turnId])] : member.handledTurnIds,
8566
+ pending
8567
+ }
8568
+ }
8569
+ }
8570
+ };
8571
+ }
8572
+ }
8573
+ if (event.type === "work.item-updated" || event.type === "work.item-claimed" || event.type === "work.comment-posted") {
8574
+ const decoded = decodeWork(event.payload);
8575
+ if (isNone2(decoded)) return state;
8576
+ const work = decoded.value;
8577
+ const project2 = readFact(state, work.projectId);
8578
+ if (project2 === null) return state;
8579
+ const members = Object.entries(project2.recovery ?? {}).filter(
8580
+ ([, member]) => member.pending !== null || member.retriedItemIds.length > 0 || (member.remindedItemIds?.length ?? 0) > 0 || (member.escalatedItemIds?.length ?? 0) > 0 || (member.unprogressedRetryItemIds?.length ?? 0) > 0
8581
+ );
8582
+ if (members.length === 0) return state;
8583
+ const pendingThreads = new Set(
8584
+ members.filter(([, member]) => member.pending !== null).map(([threadId]) => threadId)
8585
+ );
8586
+ const startedThreads = /* @__PURE__ */ new Set();
8587
+ const openWorkThreads = /* @__PURE__ */ new Set();
8588
+ if (pendingThreads.size > 0) {
8589
+ for (const [workItemId, item] of Object.entries(project2.items)) {
8590
+ const pendingAssignees = item.assigneeThreads.filter((id) => pendingThreads.has(id));
8591
+ if (pendingAssignees.length === 0) continue;
8592
+ const workable = isWorkable(project2, workItemId);
8593
+ for (const threadId of pendingAssignees) {
8594
+ if (item.stateGroup === "started") startedThreads.add(threadId);
8595
+ if (workable) openWorkThreads.add(threadId);
8596
+ }
8597
+ }
8598
+ }
8599
+ const before = previous[work.projectId]?.items[work.workItemId];
8600
+ const after = project2.items[work.workItemId];
8601
+ const recovery = { ...project2.recovery };
8602
+ let changed = false;
8603
+ for (const [threadId, member] of members) {
8604
+ const progress = event.type === "work.comment-posted" ? work.actor !== void 0 && !(work.actor.kind === "agent" && work.actor.threadId === threadId) : before?.stateGroup !== after?.stateGroup || before?.stateId !== after?.stateId;
8605
+ const assignedOpen = (id) => isAssignedOpen(project2, id, threadId);
8606
+ const retain = (id) => assignedOpen(id) && !(progress && id === work.workItemId);
8607
+ const remindedItemIds = (member.remindedItemIds ?? []).filter(retain);
8608
+ const escalatedItemIds = (member.escalatedItemIds ?? []).filter(retain);
8609
+ const unprogressedRetryItemIds = (member.unprogressedRetryItemIds ?? []).filter(retain);
8610
+ const retriedItemIds = member.retriedItemIds.filter(assignedOpen);
8611
+ const keepsPending = (member.pending?.kind === "stall" ? startedThreads : openWorkThreads).has(threadId);
8612
+ let pending = keepsPending ? member.pending : null;
8613
+ if (pending?.continuedItemIds !== void 0) {
8614
+ const continuedItemIds = pending.continuedItemIds.filter(retain);
8615
+ if (continuedItemIds.length !== pending.continuedItemIds.length)
8616
+ pending = { ...pending, continuedItemIds };
8617
+ }
8618
+ if (retriedItemIds.length === member.retriedItemIds.length && pending === member.pending && remindedItemIds.length === (member.remindedItemIds?.length ?? 0) && escalatedItemIds.length === (member.escalatedItemIds?.length ?? 0) && unprogressedRetryItemIds.length === (member.unprogressedRetryItemIds?.length ?? 0))
8619
+ continue;
8620
+ changed = true;
8621
+ recovery[threadId] = {
8622
+ ...member,
8623
+ retriedItemIds,
8624
+ remindedItemIds,
8625
+ escalatedItemIds,
8626
+ unprogressedRetryItemIds,
8627
+ pending,
8628
+ handledTurnIds: pending === null && member.pending !== null ? [.../* @__PURE__ */ new Set([...member.handledTurnIds, member.pending.turnId])] : member.handledTurnIds
8629
+ };
8630
+ }
8631
+ return changed ? { ...state, [work.projectId]: { ...project2, recovery } } : state;
8632
+ }
8633
+ return state;
8634
+ }
8635
+ function dueRecoveries(state, changedThreadId) {
8636
+ return Object.entries(state).flatMap(
8637
+ ([projectId, project2]) => Object.entries(project2.recovery ?? {}).flatMap(([threadId, member]) => {
8638
+ const memberId = project2.memberIds?.[threadId];
8639
+ const leadThreadId = project2.leadThreads?.[0];
8640
+ if (member.pending === null || changedThreadId !== void 0 && threadId !== changedThreadId && member.pending.plan?.recipientThreadId !== changedThreadId || memberId === void 0 || leadThreadId === void 0 || threadId === leadThreadId || !isMember(project2, { threadId }))
8641
+ return [];
8642
+ const stalled = member.pending.kind === "stall";
8643
+ if (member.handledTurnIds.includes(member.pending.turnId)) return [];
8644
+ const items = Object.entries(project2.items).filter(
8645
+ ([workItemId, item]) => (stalled ? item.stateGroup === "started" && item.assigneeThreads.includes(threadId) : isOpenWorkFor(project2, workItemId, threadId) && !isQueuedUnclaimed(item, threadId)) && (!stalled || (member.pending?.plan?.workItemIds.includes(workItemId) || !(member.escalatedItemIds ?? []).includes(workItemId)) && !hasOpenBlocker(project2, workItemId))
8646
+ ).map(([workItemId, item]) => ({
8647
+ workItemId,
8648
+ sequenceId: item.sequenceId,
8649
+ lastCommentId: item.lastCommentId,
8650
+ waitingForReply: item.lastActionCommentThreadId === threadId
8651
+ })).sort((a, b) => a.sequenceId - b.sequenceId);
8652
+ const deferredWakeCount = countDeferredWakes(project2, threadId);
8653
+ return items.length === 0 ? [] : [
8654
+ {
8655
+ ...member.pending,
8656
+ projectId,
8657
+ threadId,
8658
+ memberId,
8659
+ leadThreadId,
8660
+ key: JSON.stringify([threadId, member.pending.turnId]),
8661
+ alreadyRetried: items.some(
8662
+ (item) => member.retriedItemIds.includes(item.workItemId) || (member.remindedItemIds ?? []).includes(item.workItemId) || (member.escalatedItemIds ?? []).includes(item.workItemId) || (member.pending?.continuedItemIds ?? []).includes(item.workItemId)
8663
+ ),
8664
+ items,
8665
+ ...deferredWakeCount > 0 ? { deferredWakeCount } : {}
8666
+ }
8667
+ ];
8668
+ })
8669
+ );
8670
+ }
8671
+
8225
8672
  // src/runtime/wakeState.ts
8226
8673
  var WAKE_STATE_KEY = "wakes";
8227
- var WAKE_STATE_VERSION = 5;
8674
+ var WAKE_STATE_VERSION = 7;
8228
8675
  var WAKE_EVENTS = [
8229
8676
  "work.item-created",
8230
8677
  "work.item-updated",
@@ -8232,7 +8679,8 @@ var WAKE_EVENTS = [
8232
8679
  "work.relation-added",
8233
8680
  "work.relation-removed",
8234
8681
  "work.comment-posted",
8235
- "plugin.event-appended"
8682
+ "plugin.event-appended",
8683
+ "thread.session-set"
8236
8684
  ];
8237
8685
  var MemberWokenFact = Struct({
8238
8686
  version: Literal2(1),
@@ -8241,6 +8689,12 @@ var MemberWokenFact = Struct({
8241
8689
  threadId: optionalKey2(String4),
8242
8690
  principalId: optionalKey2(String4)
8243
8691
  });
8692
+ var MemberWorktreeFailedFact = Struct({
8693
+ version: Literal2(1),
8694
+ projectId: String4,
8695
+ threadId: String4,
8696
+ reason: String4
8697
+ });
8244
8698
  var emptyProject = () => ({
8245
8699
  threads: [],
8246
8700
  leadThreads: [],
@@ -8254,9 +8708,6 @@ var emptyProject = () => ({
8254
8708
  });
8255
8709
  var itemWakeKey = (workItemId, threadId, generation) => generation === 0 ? `item:${workItemId}:${threadId}` : `item:${workItemId}:${threadId}#${generation}`;
8256
8710
  var unblockedWakeKey = (workItemId, threadId, generation) => `unblocked:${workItemId}:${threadId}#${generation}`;
8257
- var hasOpenBlocker = (project2, workItemId) => (readFact(project2.blockers, workItemId) ?? []).some(
8258
- (blocker) => readFact(project2.items, blocker)?.closed !== true
8259
- );
8260
8711
  var markBlockedAfterClaim = (project2, workItemId) => ({
8261
8712
  ...project2,
8262
8713
  blockedAfterClaim: {
@@ -8275,6 +8726,20 @@ var decodeEnvelope2 = decodeUnknownOption2(
8275
8726
  pluginId: Literal2("team"),
8276
8727
  name: Literal2("team/member-woken"),
8277
8728
  data: MemberWokenFact
8729
+ }),
8730
+ Struct({
8731
+ pluginId: Literal2("team"),
8732
+ name: Literal2("team/member-worktree-failed"),
8733
+ data: MemberWorktreeFailedFact
8734
+ }),
8735
+ Struct({
8736
+ pluginId: Literal2("team"),
8737
+ name: Literal2("team/member-worktree-reported"),
8738
+ data: Struct({
8739
+ version: Literal2(1),
8740
+ projectId: String4,
8741
+ threadId: String4
8742
+ })
8278
8743
  })
8279
8744
  ])
8280
8745
  );
@@ -8288,8 +8753,10 @@ var Actor = Union2([
8288
8753
  Struct({ kind: Literal2("plugin"), pluginId: String4 }),
8289
8754
  Struct({ kind: Literals(["local-cli", "seat", "system"]) })
8290
8755
  ]);
8291
- var StateRef = Struct({ group: String4 });
8292
- var isClosed = (state) => state.group === "completed" || state.group === "cancelled";
8756
+ var StateRef = Struct({
8757
+ group: String4,
8758
+ stateId: optionalKey2(String4)
8759
+ });
8293
8760
  var itemFields = { projectId: String4, workItemId: String4 };
8294
8761
  var relationFields = {
8295
8762
  ...itemFields,
@@ -8346,7 +8813,6 @@ var decodeWorkEvent = decodeUnknownOption2(
8346
8813
  var agentThreads = (principals) => [
8347
8814
  ...new Set(principals.flatMap((p) => p.kind === "agent" ? [p.threadId] : []))
8348
8815
  ];
8349
- var isMember = (project2, recipient) => "threadId" in recipient ? project2.threads.includes(recipient.threadId) : project2.remoteMembers.includes(recipient.principalId);
8350
8816
  var commentRecipients = (project2, actor, people) => {
8351
8817
  const recipients = /* @__PURE__ */ new Map();
8352
8818
  for (const person of people) {
@@ -8358,7 +8824,7 @@ var commentRecipients = (project2, actor, people) => {
8358
8824
  return [...recipients.values()].filter((recipient) => isMember(project2, recipient));
8359
8825
  };
8360
8826
  var commentWakeKey = (commentId, recipient) => "threadId" in recipient ? `comment:${commentId}:${recipient.threadId}` : `comment:${commentId}:principal:${recipient.principalId}`;
8361
- function foldWakes(state, event) {
8827
+ function foldWorkWakes(state, event) {
8362
8828
  if (event.type === "plugin.event-appended") {
8363
8829
  const decoded2 = decodeEnvelope2(event.payload);
8364
8830
  if (isNone2(decoded2)) return state;
@@ -8376,7 +8842,16 @@ function foldWakes(state, event) {
8376
8842
  )
8377
8843
  ];
8378
8844
  const leadThreads = goal.threads.filter((thread) => thread.memberId === goal.leadMemberId).map((thread) => thread.threadId);
8379
- const next2 = { ...project4, threads, leadThreads, remoteMembers };
8845
+ const memberIds = Object.fromEntries(
8846
+ goal.threads.map((member) => [member.threadId, member.memberId])
8847
+ );
8848
+ const next2 = {
8849
+ ...project4,
8850
+ memberIds,
8851
+ threads,
8852
+ leadThreads,
8853
+ remoteMembers
8854
+ };
8380
8855
  return {
8381
8856
  ...state,
8382
8857
  [goal.projectId]: {
@@ -8385,6 +8860,20 @@ function foldWakes(state, event) {
8385
8860
  }
8386
8861
  };
8387
8862
  }
8863
+ if (fact.name === "team/member-worktree-failed" || fact.name === "team/member-worktree-reported") {
8864
+ const { projectId: projectId3, threadId } = fact.data;
8865
+ const project4 = readFact(state, projectId3) ?? emptyProject();
8866
+ const previous = project4.worktreeFailures?.[threadId];
8867
+ const failure = fact.name === "team/member-worktree-failed" ? previous ?? { reason: fact.data.reason } : previous === void 0 ? void 0 : { ...previous, reported: true };
8868
+ if (failure === void 0 || failure === previous) return state;
8869
+ return {
8870
+ ...state,
8871
+ [projectId3]: {
8872
+ ...project4,
8873
+ worktreeFailures: { ...project4.worktreeFailures, [threadId]: failure }
8874
+ }
8875
+ };
8876
+ }
8388
8877
  const { projectId: projectId2, key } = fact.data;
8389
8878
  const project3 = readFact(state, projectId2) ?? emptyProject();
8390
8879
  if (project3.pendingComments.some((comment) => comment.key === key)) {
@@ -8397,7 +8886,18 @@ function foldWakes(state, event) {
8397
8886
  };
8398
8887
  }
8399
8888
  if (project3.woken.includes(key)) return state;
8400
- return { ...state, [projectId2]: { ...project3, woken: [...project3.woken, key] } };
8889
+ const items = { ...project3.items };
8890
+ for (const [workItemId2, item] of Object.entries(items)) {
8891
+ for (const threadId of item.assigneeThreads) {
8892
+ if (item.wakeStatus?.[threadId] !== "released" && isWorkable(project3, workItemId2) && key === itemWakeKey(
8893
+ workItemId2,
8894
+ threadId,
8895
+ readFact(project3.rearms, `${workItemId2}:${threadId}`) ?? 0
8896
+ ))
8897
+ items[workItemId2] = { ...item, wakeStatus: { ...item.wakeStatus, [threadId]: "active" } };
8898
+ }
8899
+ }
8900
+ return { ...state, [projectId2]: { ...project3, items, woken: [...project3.woken, key] } };
8401
8901
  }
8402
8902
  const decoded = decodeWorkEvent(event);
8403
8903
  if (isNone2(decoded)) return state;
@@ -8415,8 +8915,15 @@ function foldWakes(state, event) {
8415
8915
  [workItemId]: {
8416
8916
  sequenceId: work.payload.sequenceId,
8417
8917
  assigneeThreads: agentThreads(work.payload.assignees),
8918
+ wakeStatus: Object.fromEntries(
8919
+ agentThreads(work.payload.assignees).map((threadId) => [
8920
+ threadId,
8921
+ "pending"
8922
+ ])
8923
+ ),
8418
8924
  claimedBy: [],
8419
- closed: isClosed(work.payload.state)
8925
+ stateGroup: work.payload.state.group,
8926
+ ...work.payload.state.stateId === void 0 ? {} : { stateId: work.payload.state.stateId }
8420
8927
  }
8421
8928
  }
8422
8929
  };
@@ -8430,6 +8937,22 @@ function foldWakes(state, event) {
8430
8937
  ...agentThreads([work.payload.assignee])
8431
8938
  ])
8432
8939
  ];
8940
+ const wakeStatus = { ...currentItem.wakeStatus };
8941
+ for (const threadId of currentItem.assigneeThreads)
8942
+ if (!assigneeThreads.includes(threadId)) wakeStatus[threadId] = "released";
8943
+ for (const threadId of assigneeThreads) {
8944
+ if (!currentItem.assigneeThreads.includes(threadId)) {
8945
+ const generation = readFact(project2.rearms, `${workItemId}:${threadId}`) ?? 0;
8946
+ wakeStatus[threadId] = project2.woken.includes(
8947
+ itemWakeKey(workItemId, threadId, generation)
8948
+ ) ? "released" : "pending";
8949
+ }
8950
+ if (wakeStatus[threadId] === "active" && (isClosed({ stateGroup: work.payload.state?.group ?? currentItem.stateGroup }) || work.payload.state?.group === "unstarted" || work.payload.state?.group === "backlog"))
8951
+ wakeStatus[threadId] = "released";
8952
+ }
8953
+ if (work.type === "work.item-claimed" && hasOpenBlocker(project2, workItemId))
8954
+ for (const threadId of agentThreads([work.payload.assignee]))
8955
+ wakeStatus[threadId] = "released";
8433
8956
  next = {
8434
8957
  ...project2,
8435
8958
  items: {
@@ -8437,8 +8960,11 @@ function foldWakes(state, event) {
8437
8960
  [workItemId]: {
8438
8961
  ...currentItem,
8439
8962
  assigneeThreads,
8963
+ wakeStatus,
8440
8964
  claimedBy: work.type === "work.item-claimed" ? [.../* @__PURE__ */ new Set([...currentItem.claimedBy, ...agentThreads([work.payload.assignee])])] : currentItem.claimedBy,
8441
- closed: work.payload.state === void 0 ? currentItem.closed : isClosed(work.payload.state)
8965
+ ...work.payload.state === void 0 ? {} : { stateGroup: work.payload.state.group },
8966
+ ...work.payload.state?.stateId === void 0 ? {} : { stateId: work.payload.state.stateId },
8967
+ lastActionCommentThreadId: null
8442
8968
  }
8443
8969
  }
8444
8970
  };
@@ -8458,16 +8984,26 @@ function foldWakes(state, event) {
8458
8984
  [workItemId]: work.type === "work.relation-added" ? [.../* @__PURE__ */ new Set([...blockers, related])] : blockers.filter((blocker) => blocker !== related)
8459
8985
  }
8460
8986
  };
8461
- if (work.type === "work.relation-added" && currentItem !== null && readFact(project2.items, related)?.closed !== true) {
8987
+ if (work.type === "work.relation-added" && currentItem !== null && !isClosed(readFact(project2.items, related))) {
8462
8988
  const woken = new Set(project2.woken);
8463
8989
  const rearms = { ...project2.rearms };
8990
+ const wakeStatus = { ...currentItem.wakeStatus };
8464
8991
  for (const threadId of currentItem.assigneeThreads) {
8465
- if (currentItem.claimedBy.includes(threadId)) continue;
8992
+ if (currentItem.claimedBy.includes(threadId)) {
8993
+ wakeStatus[threadId] = "released";
8994
+ continue;
8995
+ }
8466
8996
  const generation = rearms[`${workItemId}:${threadId}`] ?? 0;
8467
- if (woken.has(itemWakeKey(workItemId, threadId, generation)))
8997
+ if (woken.has(itemWakeKey(workItemId, threadId, generation))) {
8468
8998
  rearms[`${workItemId}:${threadId}`] = generation + 1;
8999
+ if (wakeStatus[threadId] !== "released") wakeStatus[threadId] = "pending";
9000
+ }
8469
9001
  }
8470
- next = { ...next, rearms };
9002
+ next = {
9003
+ ...next,
9004
+ rearms,
9005
+ items: { ...next.items, [workItemId]: { ...currentItem, wakeStatus } }
9006
+ };
8471
9007
  if (currentItem.claimedBy.length > 0 && !hasOpenBlocker(project2, workItemId))
8472
9008
  next = markBlockedAfterClaim(next, workItemId);
8473
9009
  }
@@ -8487,45 +9023,104 @@ function foldWakes(state, event) {
8487
9023
  })).filter(
8488
9024
  (comment) => !project2.pendingComments.some((pending) => pending.key === comment.key)
8489
9025
  );
8490
- if (comments.length === 0) return state;
8491
- next = { ...project2, pendingComments: [...project2.pendingComments, ...comments] };
9026
+ next = {
9027
+ ...project2,
9028
+ pendingComments: [...project2.pendingComments, ...comments],
9029
+ items: currentItem === null ? project2.items : {
9030
+ ...project2.items,
9031
+ [workItemId]: {
9032
+ ...currentItem,
9033
+ lastCommentId: commentId,
9034
+ lastActionCommentThreadId: actor.kind === "agent" ? actor.threadId : null
9035
+ }
9036
+ }
9037
+ };
8492
9038
  break;
8493
9039
  }
8494
9040
  }
8495
9041
  return { ...state, [projectId]: next };
8496
9042
  }
8497
- var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
9043
+ function foldWakes(state, event) {
9044
+ return foldRecovery(foldWorkWakes(state, event), event, state);
9045
+ }
9046
+ var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
9047
+ var itemWakeCandidate = (projectId, project2, workItemId, item, threadId, woken) => {
9048
+ if (!project2.threads.includes(threadId) || !isWorkable(project2, workItemId)) return null;
9049
+ const unblocked = readFact(project2.blockedAfterClaim, workItemId) ?? 0;
9050
+ const finishing = unblocked > 0 && item.claimedBy.includes(threadId);
9051
+ if (!finishing && !item.assigneeThreads.includes(threadId)) return null;
9052
+ const lead = project2.leadThreads?.includes(threadId) === true;
9053
+ const key = finishing ? unblockedWakeKey(workItemId, threadId, unblocked) : itemWakeKey(workItemId, threadId, readFact(project2.rearms, `${workItemId}:${threadId}`) ?? 0);
9054
+ if (woken.has(key) || !finishing && !lead && item.wakeStatus?.[threadId] === "released")
9055
+ return null;
9056
+ return {
9057
+ key,
9058
+ projectId,
9059
+ threadId,
9060
+ kind: finishing ? "unblocked" : "item",
9061
+ workItemId,
9062
+ sequenceId: item.sequenceId,
9063
+ // 成员的叫醒话里点名负责人(卡住时找谁);带队自己和解挡续做不带。
9064
+ ...lead ? { lead: true } : finishing || project2.leadThreads?.[0] === void 0 ? {} : { leadThreadId: project2.leadThreads[0] }
9065
+ };
9066
+ };
9067
+ var needsMemberSlot = (wake) => wake.kind === "item" && wake.lead !== true;
9068
+ function memberItemSlots(projectId, project2, woken, onlyThreadId) {
9069
+ const busy = /* @__PURE__ */ new Set();
9070
+ const slots = /* @__PURE__ */ new Map();
9071
+ for (const [workItemId, item] of Object.entries(project2.items)) {
9072
+ if (onlyThreadId !== void 0 && !item.assigneeThreads.includes(onlyThreadId)) continue;
9073
+ if (!isWorkable(project2, workItemId)) continue;
9074
+ for (const threadId of item.assigneeThreads) {
9075
+ if (onlyThreadId !== void 0 && threadId !== onlyThreadId || project2.leadThreads?.includes(threadId) || !project2.threads.includes(threadId))
9076
+ continue;
9077
+ if (item.wakeStatus?.[threadId] === "active") busy.add(threadId);
9078
+ const candidate = itemWakeCandidate(projectId, project2, workItemId, item, threadId, woken);
9079
+ if (candidate === null || !needsMemberSlot(candidate)) continue;
9080
+ const first = slots.get(threadId);
9081
+ if (first === void 0 || compareWakeItems(candidate, first) < 0)
9082
+ slots.set(threadId, candidate);
9083
+ }
9084
+ }
9085
+ for (const threadId of busy) slots.delete(threadId);
9086
+ return slots;
9087
+ }
9088
+ function isWakeDue(state, wake) {
9089
+ const project2 = readFact(state, wake.projectId);
9090
+ if (project2 === null) return false;
9091
+ if (wake.kind === "comment")
9092
+ return project2.pendingComments.some((comment) => comment.key === wake.key);
9093
+ if (!("threadId" in wake)) return false;
9094
+ const item = readFact(project2.items, wake.workItemId);
9095
+ if (item === null) return false;
9096
+ const woken = new Set(project2.woken);
9097
+ const candidate = itemWakeCandidate(
9098
+ wake.projectId,
9099
+ project2,
9100
+ wake.workItemId,
9101
+ item,
9102
+ wake.threadId,
9103
+ woken
9104
+ );
9105
+ if (candidate?.key !== wake.key) return false;
9106
+ return !needsMemberSlot(candidate) || memberItemSlots(wake.projectId, project2, woken, wake.threadId).get(wake.threadId)?.key === wake.key;
9107
+ }
8498
9108
  function dueWakes(state) {
8499
9109
  const due = [];
8500
9110
  for (const [projectId, project2] of Object.entries(state)) {
8501
9111
  const woken = new Set(project2.woken);
8502
- const threads = new Set(project2.threads);
8503
- const leads = new Set(project2.leadThreads ?? []);
9112
+ const slots = memberItemSlots(projectId, project2, woken);
8504
9113
  for (const [workItemId, item] of Object.entries(project2.items)) {
8505
- if (item.closed || hasOpenBlocker(project2, workItemId)) continue;
8506
9114
  const unblocked = readFact(project2.blockedAfterClaim, workItemId) ?? 0;
8507
9115
  const recipients = /* @__PURE__ */ new Set([
8508
9116
  ...item.assigneeThreads,
8509
9117
  ...unblocked > 0 ? item.claimedBy : []
8510
9118
  ]);
8511
9119
  for (const threadId of recipients) {
8512
- if (!threads.has(threadId)) continue;
8513
- const finishing = unblocked > 0 && item.claimedBy.includes(threadId);
8514
- const key = finishing ? unblockedWakeKey(workItemId, threadId, unblocked) : itemWakeKey(
8515
- workItemId,
8516
- threadId,
8517
- readFact(project2.rearms, `${workItemId}:${threadId}`) ?? 0
8518
- );
8519
- if (woken.has(key)) continue;
8520
- due.push({
8521
- key,
8522
- projectId,
8523
- threadId,
8524
- kind: finishing ? "unblocked" : "item",
8525
- workItemId,
8526
- sequenceId: item.sequenceId,
8527
- ...leads.has(threadId) ? { lead: true } : finishing || project2.leadThreads?.[0] === void 0 ? {} : { leadThreadId: project2.leadThreads[0] }
8528
- });
9120
+ const candidate = itemWakeCandidate(projectId, project2, workItemId, item, threadId, woken);
9121
+ if (candidate === null) continue;
9122
+ if (needsMemberSlot(candidate) && slots.get(threadId)?.key !== candidate.key) continue;
9123
+ due.push(candidate);
8529
9124
  }
8530
9125
  }
8531
9126
  for (const comment of project2.pendingComments) {
@@ -8537,7 +9132,7 @@ function dueWakes(state) {
8537
9132
  });
8538
9133
  }
8539
9134
  }
8540
- return due.sort((a, b) => compare(a.projectId, b.projectId) || compare(a.key, b.key));
9135
+ return due.sort((a, b) => compare2(a.projectId, b.projectId) || compare2(a.key, b.key));
8541
9136
  }
8542
9137
 
8543
9138
  // src/runtime/wake.ts
@@ -8549,14 +9144,19 @@ var descriptionStart = (description) => {
8549
9144
  if (first === "") return "";
8550
9145
  const points = Array.from(first);
8551
9146
  const summary = points.length > ITEM_SUMMARY_MAX_CHARS ? `${points.slice(0, ITEM_SUMMARY_MAX_CHARS).join("")}\u2026` : first;
8552
- return ` Its description starts: ${summary}`;
9147
+ return `Its description starts: ${summary}`;
9148
+ };
9149
+ var itemHeadline = (sequenceId, item) => {
9150
+ const title = item?.title ? `${sequenceId === void 0 ? "Work item" : `W${sequenceId}`}: ${item.title}.` : "";
9151
+ const parts = [title, descriptionStart(item?.description)].filter((part) => part !== "");
9152
+ return parts.length === 0 ? "" : `${parts.join(" ")} `;
8553
9153
  };
8554
- var itemText = (workItemId, sequenceId, item, leadThreadId) => `You have ${titled(workItemId, sequenceId, item?.title)}.${descriptionStart(item?.description)} Read it with work_read_items, passing workItemIds: ["${workItemId}"]. If it is blocked by items that are not done yet, do not start it; you will be told again when they are done. Otherwise claim it with work_claim_item. Before you change anything, search this repository's rules and checks with memory_search and follow what applies. Then work on it. ${MEMBER_CHECKS_RULE} ${memberStuckRule(leadThreadId)} ${handbookMessage("memberBrief")} ${PERSON_MESSAGE_RULE}`;
9154
+ var itemText = (workItemId, sequenceId, item, leadThreadId) => `${itemHeadline(sequenceId, item)}You have ${workLabel(workItemId, sequenceId)}. Read it with work_read_items, passing workItemIds: ["${workItemId}"]. If it is blocked by items that are not done yet, do not start it; you will be told again when they are done. Otherwise claim it with work_claim_item. Before you change anything, search this repository's rules and checks with memory_search and follow what applies. Then work on it. ${MEMBER_CHECKS_RULE} ${memberStuckRule(leadThreadId)} ${handbookMessage("memberBrief")} ${PERSON_MESSAGE_RULE}`;
8555
9155
  var leadItemText = (workItemId, sequenceId, title) => `You have ${titled(workItemId, sequenceId, title)}. You lead this team, so this item is the goal itself: do not claim it or do its work yourself. Read it with work_read_items, passing workItemIds: ["${workItemId}"]. If it has no work items yet, split it as your team brief says. ${LEAD_SPLIT_RULE} ${LEAD_OWN_ITEM_RULE} Record dependencies and assign each item to one member as the brief describes. ${LEAD_NO_MEMBER_TASKS} ${PERSON_MESSAGE_RULE}`;
8556
9156
  var leadUnblockedText = (workItemId, sequenceId, title) => `Everything blocking ${titled(workItemId, sequenceId, title)} is done now. Read those items with work_read_items and check the results. ${LEAD_SEND_BACK_RULE} When all of them are acceptable, mark this item done with work_update_item. ${LEAD_NO_MEMBER_TASKS}`;
8557
9157
  var unblockedText = (workItemId, sequenceId, title) => `Everything blocking ${titled(workItemId, sequenceId, title)} is done now. Read those items with work_read_items, finish this one, and mark it done with work_update_item.`;
8558
9158
  var commentText2 = (workItemId, sequenceId) => `There is a new comment for you on ${workLabel(workItemId, sequenceId)}. Read it with work_list_comments, passing workItemId: "${workItemId}", and reply there if needed.`;
8559
- function startWakeRuntime(ctx, options = {}) {
9159
+ function startWakeRuntime(ctx, goals, options = {}) {
8560
9160
  const wakes = ctx.derivedState({
8561
9161
  key: WAKE_STATE_KEY,
8562
9162
  version: WAKE_STATE_VERSION,
@@ -8564,6 +9164,7 @@ function startWakeRuntime(ctx, options = {}) {
8564
9164
  init: () => ({}),
8565
9165
  apply: foldWakes
8566
9166
  });
9167
+ const worktrees = createMemberWorktreeLane(wakes, ctx.events);
8567
9168
  const hash2 = options.hash ?? stableHash;
8568
9169
  let pending = Promise.resolve();
8569
9170
  const readItem = async (projectId, workItemId) => {
@@ -8571,15 +9172,18 @@ function startWakeRuntime(ctx, options = {}) {
8571
9172
  return result2.items.find((item) => item.workItemId === workItemId);
8572
9173
  };
8573
9174
  const execute = async () => {
8574
- const state = await wakes.get();
9175
+ let state = await wakes.get();
8575
9176
  for (const wake of dueWakes(state)) {
9177
+ if (!isWakeDue(state, wake)) continue;
8576
9178
  try {
8577
9179
  const id = hash2(wake.key);
9180
+ const worktree = memberWorktreeFor(goals, state[wake.projectId], wake.projectId, wake);
8578
9181
  const delivered = await deliverWake(ctx, {
8579
9182
  projectId: wake.projectId,
8580
9183
  recipient: wake,
8581
9184
  requestId: `team-wake:${id}`,
8582
9185
  messageId: `team-wake-msg:${id}`,
9186
+ memberWorktree: worktree === void 0 ? void 0 : { ...worktree, lane: worktrees },
8583
9187
  text: async () => {
8584
9188
  if ("principalId" in wake || wake.kind === "comment")
8585
9189
  return commentText2(wake.workItemId, wake.sequenceId);
@@ -8606,7 +9210,7 @@ function startWakeRuntime(ctx, options = {}) {
8606
9210
  ..."principalId" in wake ? { principalId: wake.principalId } : { threadId: wake.threadId }
8607
9211
  }
8608
9212
  });
8609
- await wakes.get({ throughSequence: receipt.sequence });
9213
+ state = await wakes.get({ throughSequence: receipt.sequence });
8610
9214
  } catch (error) {
8611
9215
  ctx.log?.warn("team member wake failed", { key: wake.key, error });
8612
9216
  }
@@ -8620,10 +9224,216 @@ function startWakeRuntime(ctx, options = {}) {
8620
9224
  );
8621
9225
  return result2;
8622
9226
  };
8623
- ctx.reactor({ name: "team-wake", events: WAKE_EVENTS, handle: () => flush() });
9227
+ ctx.reactor({
9228
+ name: "team-wake",
9229
+ events: WAKE_EVENTS,
9230
+ handle: (event) => event.type === "thread.session-set" || isRecoveryFact(event) ? Promise.resolve() : flush()
9231
+ });
8624
9232
  const ready = flush();
8625
9233
  void ready.catch((error) => ctx.log?.warn("team wake catch-up failed", { error }));
8626
- return { flush, ready };
9234
+ return { flush, ready, state: wakes, worktrees };
9235
+ }
9236
+
9237
+ // src/runtime/recovery.ts
9238
+ var readStallExecution = async (ctx, threadId) => {
9239
+ const shell = await ctx.threads.get({ threadId });
9240
+ const summary = await ctx.threads.execution.summary({ threadId, limit: 1 });
9241
+ return { ...shell, queuePaused: summary.queuePaused, summary };
9242
+ };
9243
+ var itemLabel = (item) => `W${item.sequenceId} (workItemId ${item.workItemId})`;
9244
+ var deferredText = (recovery) => recovery.deferredWakeCount === void 0 ? "" : `They also have ${recovery.deferredWakeCount} assigned ${recovery.deferredWakeCount === 1 ? "item that has" : "items that have"} not been woken yet. `;
9245
+ var leadText = (recovery, name, paused) => `Member ${name} (threadId ${recovery.threadId}) stopped because of an upstream error (${recovery.category}). ${recovery.items.map(itemLabel).join(", ")} ${recovery.items.length === 1 ? "is" : "are"} still open. ${deferredText(recovery)}${recovery.alreadyRetried ? "An automatic continuation has already been attempted for this work during these consecutive failures. " : ""}${leadInstructions(paused)}`;
9246
+ var commentInstructions = (recovery) => recovery.items.map(
9247
+ (item) => item.lastCommentId === void 0 ? `${itemLabel(item)} has no comments yet. Continue from your existing workspace changes and turn history.` : `For ${itemLabel(item)}, the latest comment is commentId ${item.lastCommentId}. Read work_list_comments with workItemId: "${item.workItemId}" and continue from that comment and your existing workspace changes; do not start over.`
9248
+ ).join("\n");
9249
+ var leadInstructions = (paused = false) => `${paused ? "Their sending is paused; a person needs to click Resume sending. " : ""}You can post a comment mentioning the member with work_post_comment to ask them to continue (the comment wakes them), reassign the work with work_update_item, or report the problem to the person.${paused ? " A comment will wait until the person resumes sending." : ""}`;
9250
+ var resumeText = (recovery) => `Your previous turn was interrupted by an upstream error (${recovery.category}). Continue ${recovery.items.map(itemLabel).join(", ")} from where you stopped.
9251
+ ${commentInstructions(recovery)}`;
9252
+ var remindText = (recovery) => `Your turn ended normally, but ${recovery.items.map(itemLabel).join(", ")} ${recovery.items.length === 1 ? "is" : "are"} still in progress. If finished, mark the work done with work_update_item. If blocked, explain where you are stuck on the item with work_post_comment. Otherwise continue from where you stopped; do not start over.
9253
+ ${commentInstructions(recovery)}`;
9254
+ var stallLeadText = (recovery, name) => `Member ${name} (threadId ${recovery.threadId}) ended this turn normally, but ${recovery.items.map(itemLabel).join(", ")} ${recovery.items.length === 1 ? "is" : "are"} still open. ${deferredText(recovery)}${recovery.alreadyRetried ? "They have already been reminded once to continue this work, with no progress since. " : ""}${recovery.items.filter((item) => item.waitingForReply).map(
9255
+ (item) => `Their last action on ${itemLabel(item)} was their own comment (commentId ${item.lastCommentId}); reply to it or arrange another member to help. `
9256
+ ).join("")}${leadInstructions()}`;
9257
+ function startRecoveryRuntime(ctx, goals, state, worktrees) {
9258
+ let pending = Promise.resolve();
9259
+ let disposed = false;
9260
+ const execute = async (scope2) => {
9261
+ if (disposed) return;
9262
+ const current = await state.get();
9263
+ let projects = current;
9264
+ if (scope2?.kind === "project") {
9265
+ const project2 = readFact(current, scope2.projectId);
9266
+ projects = project2 === null ? {} : { [scope2.projectId]: project2 };
9267
+ } else if (scope2?.kind === "thread") {
9268
+ projects = Object.fromEntries(
9269
+ Object.entries(current).filter(
9270
+ ([, project2]) => Object.entries(project2.recovery ?? {}).some(
9271
+ ([threadId, member]) => threadId === scope2.threadId || member.pending?.plan?.recipientThreadId === scope2.threadId
9272
+ )
9273
+ )
9274
+ );
9275
+ }
9276
+ const recoveries = dueRecoveries(
9277
+ projects,
9278
+ scope2?.kind === "thread" ? scope2.threadId : void 0
9279
+ );
9280
+ const wakingThreads = new Set(
9281
+ recoveries.some((recovery) => recovery.kind === "stall") ? dueWakes(projects).flatMap((wake) => "threadId" in wake ? [wake.threadId] : []) : []
9282
+ );
9283
+ for (const recovery of recoveries) {
9284
+ try {
9285
+ const stalled = recovery.kind === "stall";
9286
+ const execution = stalled ? await readStallExecution(ctx, recovery.threadId) : await ctx.projections.getThreadExecution(recovery.threadId);
9287
+ const queuedContinuation = recovery.plan?.resumeQueue === true;
9288
+ if (execution === null || execution.archivedAt !== null || execution.session == null || !queuedContinuation && (execution.session.activeTurnId !== null || execution.latestTurn?.turnId !== recovery.turnId || (stalled ? !["ready", "idle", "stopped"].includes(execution.session.status) || execution.latestTurn.state !== "completed" || execution.latestTurn.completedAt === null : execution.session.status !== "error" || execution.latestTurn.state !== "error")))
9289
+ continue;
9290
+ if ("summary" in execution) {
9291
+ if (execution.summary.queuePaused || execution.summary.pendingApprovalCount > 0 || execution.summary.pendingUserInputCount > 0 || execution.summary.queue.length > 0 || (execution.pendingAsyncQuestionCount ?? 0) > 0 || // 成员给人发了消息、人还没回:他在等回答,不提醒也不叫负责人。
9292
+ (execution.awaitingPersonReplyCount ?? 0) > 0)
9293
+ continue;
9294
+ if (wakingThreads.has(recovery.threadId) || Object.values(projects).some(
9295
+ (project2) => Object.values(project2.recovery ?? {}).some(
9296
+ (member) => member.pending?.plan?.recipientThreadId === recovery.threadId && member.pending !== current[recovery.projectId]?.recovery?.[recovery.threadId]?.pending
9297
+ )
9298
+ ))
9299
+ continue;
9300
+ }
9301
+ const id = stableHash(recovery.key);
9302
+ let plan = recovery.plan;
9303
+ let tryQueueResume = false;
9304
+ const requestPlan = async (next) => {
9305
+ const receipt2 = await ctx.events.append({
9306
+ commandId: `team-recovery-requested:${id}:${next.action}${next.resumeQueue === true ? "-queue" : ""}`,
9307
+ name: "member-recovery-requested",
9308
+ aggregate: { kind: "project", id: recovery.projectId },
9309
+ data: {
9310
+ version: 1,
9311
+ projectId: recovery.projectId,
9312
+ threadId: recovery.threadId,
9313
+ turnId: recovery.turnId,
9314
+ failureSequence: recovery.sequence,
9315
+ plan: next
9316
+ }
9317
+ });
9318
+ await state.get({ throughSequence: receipt2.sequence });
9319
+ };
9320
+ if (plan === void 0 || plan.action === "resume" && execution.queuePaused && plan.resumeQueue !== true) {
9321
+ const resume = !stalled && (!recovery.alreadyRetried || plan?.action === "resume") && (recovery.category === "transport" || recovery.category === "rate-limit" && !execution.queuePaused);
9322
+ const remind = stalled && !recovery.alreadyRetried && !recovery.items.some((item) => item.waitingForReply);
9323
+ const goal = resume || remind ? null : await goals.read(recovery.projectId);
9324
+ const name = goal?.members.find((member) => member.threadId === recovery.threadId)?.name ?? recovery.memberId;
9325
+ plan = {
9326
+ action: stalled ? remind ? "remind" : "stall-lead" : resume ? "resume" : "lead",
9327
+ recipientThreadId: resume || remind ? recovery.threadId : recovery.leadThreadId,
9328
+ text: stalled ? remind ? remindText(recovery) : stallLeadText(recovery, name) : resume ? resumeText(recovery) : leadText(recovery, name, execution.queuePaused),
9329
+ workItemIds: recovery.items.map((item) => item.workItemId),
9330
+ ...resume && execution.queuePaused ? { resumeQueue: true } : {}
9331
+ };
9332
+ await requestPlan(plan);
9333
+ tryQueueResume = plan.resumeQueue === true;
9334
+ }
9335
+ if (plan.resumeQueue === true && execution.queuePaused) {
9336
+ const project2 = (await state.get())[recovery.projectId];
9337
+ if (project2 === void 0 || !isMember(project2, { threadId: recovery.threadId }) || project2.leadThreads?.includes(recovery.threadId) || project2.recovery?.[recovery.threadId]?.pending?.turnId !== recovery.turnId)
9338
+ continue;
9339
+ let resumed = false;
9340
+ let outcome = "already-attempted";
9341
+ if (tryQueueResume) {
9342
+ try {
9343
+ const receipt2 = await ctx.threads.turnQueue.resume({
9344
+ requestId: `team-recovery-queue:${id}`,
9345
+ threadId: recovery.threadId
9346
+ });
9347
+ resumed = receipt2.status === "accepted";
9348
+ outcome = "noop";
9349
+ } catch {
9350
+ outcome = "rejected";
9351
+ }
9352
+ }
9353
+ if (!resumed) {
9354
+ const latest = await ctx.projections.getThreadExecution(recovery.threadId).catch(() => null);
9355
+ resumed = latest?.queuePaused === false;
9356
+ ctx.log.warn("team member queue recovery refused", {
9357
+ threadId: recovery.threadId,
9358
+ turnId: recovery.turnId,
9359
+ outcome,
9360
+ queuePaused: !resumed
9361
+ });
9362
+ }
9363
+ if (!resumed) {
9364
+ const goal = await goals.read(recovery.projectId);
9365
+ const name = goal?.members.find((member) => member.threadId === recovery.threadId)?.name ?? recovery.memberId;
9366
+ plan = {
9367
+ action: "lead",
9368
+ recipientThreadId: recovery.leadThreadId,
9369
+ text: leadText(recovery, name, true),
9370
+ workItemIds: plan.workItemIds
9371
+ };
9372
+ await requestPlan(plan);
9373
+ }
9374
+ }
9375
+ const worktree = memberWorktreeFor(goals, current[recovery.projectId], recovery.projectId, {
9376
+ threadId: plan.recipientThreadId
9377
+ });
9378
+ const delivered = await deliverWake(ctx, {
9379
+ projectId: recovery.projectId,
9380
+ recipient: { threadId: plan.recipientThreadId },
9381
+ requestId: `team-recovery:${id}:${plan.action}`,
9382
+ messageId: `team-recovery-msg:${id}:${plan.action}`,
9383
+ text: plan.text,
9384
+ memberWorktree: worktree === void 0 ? void 0 : { ...worktree, lane: worktrees }
9385
+ });
9386
+ if (!delivered) continue;
9387
+ const receipt = await ctx.events.append({
9388
+ commandId: `team-recovered:${id}:${plan.action}`,
9389
+ name: "member-recovery-delivered",
9390
+ aggregate: { kind: "project", id: recovery.projectId },
9391
+ data: {
9392
+ version: 1,
9393
+ projectId: recovery.projectId,
9394
+ threadId: recovery.threadId,
9395
+ turnId: recovery.turnId,
9396
+ failureSequence: recovery.sequence,
9397
+ action: plan.action,
9398
+ workItemIds: plan.workItemIds
9399
+ }
9400
+ });
9401
+ await state.get({ throughSequence: receipt.sequence });
9402
+ } catch (error) {
9403
+ ctx.log.warn("team member recovery failed", { key: recovery.key, error });
9404
+ }
9405
+ }
9406
+ };
9407
+ const flush = (scope2) => {
9408
+ const result2 = pending.then(() => execute(scope2));
9409
+ pending = result2.catch(() => void 0);
9410
+ return result2;
9411
+ };
9412
+ ctx.reactor({
9413
+ name: "team-member-recovery",
9414
+ events: [
9415
+ "thread.session-set",
9416
+ "thread.turn-queue-paused",
9417
+ "plugin.event-appended",
9418
+ "work.item-updated",
9419
+ "work.item-claimed",
9420
+ "work.comment-posted",
9421
+ "work.relation-added",
9422
+ "work.relation-removed",
9423
+ "thread.turn-intent-queued",
9424
+ "thread.turn-intent-transitioned",
9425
+ "thread.turn-queue-updated",
9426
+ "thread.request-resolved",
9427
+ "thread.user-input-pending"
9428
+ ],
9429
+ handle: (event) => flush(recoveryEventScopeOf(event))
9430
+ });
9431
+ ctx.onDispose(() => {
9432
+ disposed = true;
9433
+ });
9434
+ const ready = flush();
9435
+ void ready.catch((error) => ctx.log.warn("team member recovery catch-up failed", { error }));
9436
+ return { ready, flush };
8627
9437
  }
8628
9438
 
8629
9439
  // src/handbook/content.ts
@@ -8916,7 +9726,7 @@ var terminalFailure = (error) => {
8916
9726
  if (!rejected && !invalid2) return null;
8917
9727
  return error instanceof Error ? error.message : "detail" in error ? String(error.detail) : String(reason);
8918
9728
  };
8919
- function startHandbookRuntime(ctx) {
9729
+ function startHandbookRuntime(ctx, worktrees) {
8920
9730
  const state = ctx.derivedState({
8921
9731
  key: "handbook",
8922
9732
  version: 5,
@@ -8995,16 +9805,24 @@ function startHandbookRuntime(ctx) {
8995
9805
  };
8996
9806
  const remind = async () => {
8997
9807
  const snapshot = await state.get();
9808
+ const wakeState = Object.keys(snapshot.pendingReminders).length === 0 ? void 0 : await worktrees.lane.state.get();
8998
9809
  for (const [key, reminder] of Object.entries(snapshot.pendingReminders)) {
8999
9810
  const { projectId, release, member } = reminder;
9000
9811
  try {
9001
9812
  const id = stableHash(key);
9813
+ const worktree = memberWorktreeFor(
9814
+ worktrees.goals,
9815
+ wakeState?.[projectId],
9816
+ projectId,
9817
+ member.recipient
9818
+ );
9002
9819
  const delivered = await deliverWake(ctx, {
9003
9820
  projectId,
9004
9821
  recipient: member.recipient,
9005
9822
  requestId: `team-handbook:${id}`,
9006
9823
  messageId: `team-handbook-msg:${id}`,
9007
- text: handbookMessage("reminder", release)
9824
+ text: handbookMessage("reminder", release),
9825
+ memberWorktree: worktree === void 0 ? void 0 : { ...worktree, lane: worktrees.lane }
9008
9826
  });
9009
9827
  if (!delivered) continue;
9010
9828
  const previousKey = JSON.stringify([release, member.memberId]);
@@ -9329,7 +10147,7 @@ function registerHandbookPanel(ctx, runtime) {
9329
10147
  // src/index.ts
9330
10148
  var index_default = definePlugin({
9331
10149
  id: "team",
9332
- apiVersion: 28,
10150
+ apiVersion: 30,
9333
10151
  activate(ctx) {
9334
10152
  const rosters = createRosterService(ctx);
9335
10153
  registerRosterCommands(ctx, rosters);
@@ -9337,8 +10155,9 @@ var index_default = definePlugin({
9337
10155
  registerGoalCommands(ctx, goals);
9338
10156
  registerBoardCommands(ctx, goals, rosters);
9339
10157
  registerDetailCommands(ctx, goals);
9340
- startWakeRuntime(ctx);
9341
- const handbook = startHandbookRuntime(ctx);
10158
+ const wakes = startWakeRuntime(ctx, goals);
10159
+ startRecoveryRuntime(ctx, goals, wakes.state, wakes.worktrees);
10160
+ const handbook = startHandbookRuntime(ctx, { lane: wakes.worktrees, goals });
9342
10161
  registerHandbook(ctx, handbook);
9343
10162
  registerHandbookPanel(ctx, handbook);
9344
10163
  }