@ycodium-ai/plugin-team 0.2.3244 → 0.2.3252

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;
@@ -7065,6 +7065,7 @@ var en_default = {
7065
7065
  toolkitsOff: "The lead needs the board and project memory. Turn on both switches above.",
7066
7066
  toolkitsOn: "The board and project memory are on for this project.",
7067
7067
  staleRevision: "The roster was changed elsewhere and has been reloaded.",
7068
+ updatedElsewhere: "The roster was changed elsewhere. Saving now discards your unsaved edits \u2014 copy them somewhere safe first.",
7068
7069
  loadFailed: "The roster could not be loaded. Try reloading it.",
7069
7070
  saveFailed: "The roster could not be saved. Try again.",
7070
7071
  invalidPayload: "Check the roster fields and try again.",
@@ -7165,7 +7166,14 @@ function handbookMessage(key, release) {
7165
7166
  }
7166
7167
 
7167
7168
  // src/domain/leadBrief.ts
7168
- var PERSON_MESSAGE_RULE = "Send a message to the person only when you need their answer; put progress and updates in work item comments with work_post_comment instead.";
7169
+ var PERSON_MESSAGE_RULE = "Send a message to the person only for a product decision you cannot make yourself; put progress, updates and technical problems in work item comments with work_post_comment instead.";
7170
+ var MEMBER_CHECKS_RULE = "Before you mark an item done, run the checks that this project's rules name for the code you changed, and fix every failure. Do not mark an item done while a check still fails; say in a work item comment what still fails instead.";
7171
+ var memberStuckRule = (leadThreadId) => {
7172
+ const mention = leadThreadId === void 0 ? "" : ` that mentions your lead (mentions: [{ "kind": "agent", "threadId": "${leadThreadId}" }])`;
7173
+ const reply = leadThreadId === void 0 ? "" : " Your lead will reply or change the plan.";
7174
+ return `If the same error still fails after you have tried to fix it twice, stop and post a work item comment with work_post_comment${mention} saying what fails and what you tried.${reply}`;
7175
+ };
7176
+ var LEAD_STUCK_RULE = "When a member mentions you in a comment because they are stuck, answer in that item's comments: give a direction, split the item, or reassign it.";
7169
7177
  var LEAD_SPLIT_RULE = "Use work_create_item to split the goal into a few work items: one item per task, with a clear action in the title. Members do not see this conversation or your goal, so put everything a member needs into the description: the deliverable, the constraints, and how to hand the work in. Create them without assignees.";
7170
7178
  var LEAD_OWN_ITEM_RULE = "If a work item for this goal is assigned to you, also record it as blocked by every item you create. When they are all done you will be told; then check the results and mark that item done with work_update_item.";
7171
7179
  var LEAD_SEND_BACK_RULE = "If a finished item is not acceptable, send it back: use work_update_item to set its state back to an open one (see work_list_states), and change its assignees too if someone else should redo it. Then say what is wrong in a comment with work_post_comment, mentioning the member.";
@@ -7196,7 +7204,8 @@ function leadBrief(goal, roster) {
7196
7204
  'Then use work_add_relation with type "blocked_by" to record dependencies: the dependent workItemId is blocked by the prerequisite relatedWorkItemId.',
7197
7205
  LEAD_OWN_ITEM_RULE,
7198
7206
  LEAD_SEND_BACK_RULE,
7199
- `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}`,
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>" }]. 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}`,
7200
7209
  ...remoteMembers.length === 0 ? [] : [
7201
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.',
7202
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."
@@ -7209,6 +7218,7 @@ function leadBrief(goal, roster) {
7209
7218
 
7210
7219
  // src/domain/goals.ts
7211
7220
  var REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
7221
+ var isTeamRequestId = (requestId) => REQUEST_ID_PATTERN.test(requestId);
7212
7222
  var GOAL_MAX_LENGTH = 4e3;
7213
7223
  var goalText = (raw) => {
7214
7224
  const text = raw.trim();
@@ -7324,7 +7334,7 @@ function createGoalService(ctx, rosterService, options = {}) {
7324
7334
  const input = decoded.value;
7325
7335
  const text = goalText(input.goal);
7326
7336
  if (text === null) return rejectGoal("invalid-goal");
7327
- if (!REQUEST_ID_PATTERN.test(input.requestId)) return rejectGoal("invalid-request-id");
7337
+ if (!isTeamRequestId(input.requestId)) return rejectGoal("invalid-request-id");
7328
7338
  const roster = await rosterService.read(input.projectId);
7329
7339
  if (roster === null) return rejectGoal("no-roster");
7330
7340
  const project2 = await ctx.projects.get({ projectId: input.projectId });
@@ -8213,9 +8223,342 @@ async function deliverWake(ctx, input) {
8213
8223
  return true;
8214
8224
  }
8215
8225
 
8226
+ // src/runtime/wakeQueries.ts
8227
+ var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
8228
+ var compareWakeItems = (a, b) => a.sequenceId - b.sequenceId || compare(a.workItemId, b.workItemId);
8229
+ var isClosed = (item) => item?.stateGroup === "completed" || item?.stateGroup === "cancelled";
8230
+ var hasOpenBlocker = (project2, workItemId) => (readFact(project2.blockers, workItemId) ?? []).some(
8231
+ (blocker) => !isClosed(readFact(project2.items, blocker))
8232
+ );
8233
+ var isAssignedOpen = (project2, workItemId, threadId) => {
8234
+ const item = readFact(project2.items, workItemId);
8235
+ return item !== null && item !== void 0 && !isClosed(item) && item.assigneeThreads.includes(threadId);
8236
+ };
8237
+ var isWorkable = (project2, workItemId) => !isClosed(readFact(project2.items, workItemId)) && !hasOpenBlocker(project2, workItemId);
8238
+ var isOpenWorkFor = (project2, workItemId, threadId) => isAssignedOpen(project2, workItemId, threadId) && !hasOpenBlocker(project2, workItemId);
8239
+ var isMember = (project2, recipient) => "threadId" in recipient ? project2.threads.includes(recipient.threadId) : project2.remoteMembers.includes(recipient.principalId);
8240
+ var isQueuedUnclaimed = (item, threadId) => item.wakeStatus?.[threadId] === "pending" && !item.claimedBy.includes(threadId);
8241
+ var isDeferredWake = (project2, item, threadId) => project2.threads.includes(threadId) && !project2.leadThreads?.includes(threadId) && item.assigneeThreads.includes(threadId) && isQueuedUnclaimed(item, threadId) && !isClosed(item);
8242
+ function countDeferredWakes(project2, threadId) {
8243
+ let count = 0;
8244
+ for (const workItemId in project2.items) {
8245
+ const item = readFact(project2.items, workItemId);
8246
+ if (item !== null && isDeferredWake(project2, item, threadId)) count++;
8247
+ }
8248
+ return count;
8249
+ }
8250
+
8251
+ // src/runtime/recoveryState.ts
8252
+ var RecoveryPlan = Struct({
8253
+ action: Literals(["resume", "lead", "remind", "stall-lead"]),
8254
+ recipientThreadId: String4,
8255
+ text: String4,
8256
+ workItemIds: ArraySchema(String4),
8257
+ resumeQueue: optionalKey2(Literal2(true))
8258
+ });
8259
+ var REQUESTED = "team/member-recovery-requested";
8260
+ var DELIVERED = "team/member-recovery-delivered";
8261
+ var decodeRecoveryFact = decodeUnknownOption2(
8262
+ Struct({
8263
+ pluginId: Literal2("team"),
8264
+ name: Literals([REQUESTED, DELIVERED])
8265
+ })
8266
+ );
8267
+ var isRecoveryFact = (event) => event.type === "plugin.event-appended" && isSome2(decodeRecoveryFact(event.payload));
8268
+ var decodeGoal = decodeUnknownOption2(
8269
+ Struct({
8270
+ pluginId: Literal2("team"),
8271
+ name: Literal2("team/goal-started"),
8272
+ data: GoalStartedFact
8273
+ })
8274
+ );
8275
+ var decodeDelivered = decodeUnknownOption2(
8276
+ Struct({
8277
+ pluginId: Literal2("team"),
8278
+ name: Literal2(DELIVERED),
8279
+ data: Struct({
8280
+ version: Literal2(1),
8281
+ projectId: String4,
8282
+ threadId: String4,
8283
+ turnId: String4,
8284
+ failureSequence: Number5,
8285
+ action: RecoveryPlan.fields.action,
8286
+ workItemIds: ArraySchema(String4)
8287
+ })
8288
+ })
8289
+ );
8290
+ var decodeRequested = decodeUnknownOption2(
8291
+ Struct({
8292
+ pluginId: Literal2("team"),
8293
+ name: Literal2(REQUESTED),
8294
+ data: Struct({
8295
+ version: Literal2(1),
8296
+ projectId: String4,
8297
+ threadId: String4,
8298
+ turnId: String4,
8299
+ failureSequence: Number5,
8300
+ plan: RecoveryPlan
8301
+ })
8302
+ })
8303
+ );
8304
+ var decodeSession = decodeUnknownOption2(
8305
+ Struct({
8306
+ threadId: String4,
8307
+ session: Struct({
8308
+ status: String4,
8309
+ activeTurnId: NullOr(String4),
8310
+ failureCategory: optionalKey2(String4)
8311
+ })
8312
+ })
8313
+ );
8314
+ var decodeWork = decodeUnknownOption2(
8315
+ Struct({
8316
+ projectId: String4,
8317
+ workItemId: String4,
8318
+ actor: optionalKey2(
8319
+ Struct({ kind: String4, threadId: optionalKey2(String4) })
8320
+ )
8321
+ })
8322
+ );
8323
+ function recoveryEventScopeOf(event) {
8324
+ if (event.aggregateKind === "project") return { kind: "project", projectId: event.aggregateId };
8325
+ if (event.aggregateKind === "thread") return { kind: "thread", threadId: event.aggregateId };
8326
+ }
8327
+ function foldRecovery(state, event, previous) {
8328
+ if (event.type === "plugin.event-appended") {
8329
+ const goalFact = decodeGoal(event.payload);
8330
+ if (isSome2(goalFact)) {
8331
+ const { goal } = goalFact.value.data;
8332
+ const project2 = readFact(state, goal.projectId);
8333
+ if (project2 === null) return state;
8334
+ const recovery = {};
8335
+ for (const threadId of project2.threads) {
8336
+ if ((project2.leadThreads ?? []).includes(threadId)) continue;
8337
+ recovery[threadId] = {
8338
+ turnId: null,
8339
+ status: null,
8340
+ retriedItemIds: [],
8341
+ handledTurnIds: [],
8342
+ pending: null
8343
+ };
8344
+ }
8345
+ return { ...state, [goal.projectId]: { ...project2, recovery } };
8346
+ }
8347
+ const requested = decodeRequested(event.payload);
8348
+ if (isSome2(requested)) {
8349
+ const fact = requested.value.data;
8350
+ const project2 = readFact(state, fact.projectId);
8351
+ const member = project2?.recovery?.[fact.threadId];
8352
+ if (project2 === null || member === void 0 || member.pending?.turnId !== fact.turnId)
8353
+ return state;
8354
+ const retriedItemIds = fact.plan.action === "resume" ? [.../* @__PURE__ */ new Set([...member.retriedItemIds, ...fact.plan.workItemIds])].filter(
8355
+ (id) => isAssignedOpen(project2, id, fact.threadId)
8356
+ ) : member.retriedItemIds;
8357
+ return {
8358
+ ...state,
8359
+ [fact.projectId]: {
8360
+ ...project2,
8361
+ recovery: {
8362
+ ...project2.recovery,
8363
+ [fact.threadId]: {
8364
+ ...member,
8365
+ retriedItemIds,
8366
+ ...fact.plan.action === "resume" ? {
8367
+ unprogressedRetryItemIds: [
8368
+ .../* @__PURE__ */ new Set([
8369
+ ...member.unprogressedRetryItemIds ?? [],
8370
+ ...fact.plan.workItemIds
8371
+ ])
8372
+ ]
8373
+ } : {},
8374
+ ...fact.plan.action === "remind" ? {
8375
+ remindedItemIds: [
8376
+ .../* @__PURE__ */ new Set([...member.remindedItemIds ?? [], ...fact.plan.workItemIds])
8377
+ ]
8378
+ } : {},
8379
+ ...fact.plan.action === "stall-lead" ? {
8380
+ escalatedItemIds: [
8381
+ .../* @__PURE__ */ new Set([...member.escalatedItemIds ?? [], ...fact.plan.workItemIds])
8382
+ ]
8383
+ } : {},
8384
+ pending: { ...member.pending, plan: fact.plan }
8385
+ }
8386
+ }
8387
+ }
8388
+ };
8389
+ }
8390
+ const delivered = decodeDelivered(event.payload);
8391
+ if (isSome2(delivered)) {
8392
+ const fact = delivered.value.data;
8393
+ const project2 = readFact(state, fact.projectId);
8394
+ const member = project2?.recovery?.[fact.threadId];
8395
+ if (project2 === null || member === void 0 || member.handledTurnIds.includes(fact.turnId))
8396
+ return state;
8397
+ return {
8398
+ ...state,
8399
+ [fact.projectId]: {
8400
+ ...project2,
8401
+ recovery: {
8402
+ ...project2.recovery,
8403
+ [fact.threadId]: {
8404
+ ...member,
8405
+ handledTurnIds: [...member.handledTurnIds, fact.turnId],
8406
+ pending: member.pending?.turnId === fact.turnId ? null : member.pending
8407
+ }
8408
+ }
8409
+ }
8410
+ };
8411
+ }
8412
+ }
8413
+ if (event.type === "thread.session-set") {
8414
+ const decoded = decodeSession(event.payload);
8415
+ if (isNone2(decoded)) return state;
8416
+ const { threadId, session } = decoded.value;
8417
+ for (const [projectId, project2] of Object.entries(state)) {
8418
+ const member = project2.recovery?.[threadId];
8419
+ if (member === void 0 || !isMember(project2, { threadId })) continue;
8420
+ const turnId = session.activeTurnId ?? member.turnId;
8421
+ if (session.status !== "running" && turnId !== null && member.handledTurnIds.includes(turnId))
8422
+ return state;
8423
+ const succeeded = (session.status === "ready" || session.status === "idle") && member.status === "running";
8424
+ const failed = session.status === "error" && turnId !== null;
8425
+ if (member.status === session.status && member.turnId === turnId && !failed) return state;
8426
+ const hasStarted = Object.values(project2.items).some(
8427
+ (item) => item.stateGroup === "started" && item.assigneeThreads.includes(threadId)
8428
+ );
8429
+ const hasOpenWork = Object.keys(project2.items).some(
8430
+ (id) => isOpenWorkFor(project2, id, threadId)
8431
+ );
8432
+ const queuedContinuation = member.pending?.plan?.resumeQueue === true && !failed;
8433
+ 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 ? {
8434
+ turnId,
8435
+ category: "normal-end",
8436
+ kind: "stall",
8437
+ sequence: event.sequence,
8438
+ continuedItemIds: member.unprogressedRetryItemIds ?? []
8439
+ } : session.status === "running" || succeeded ? null : member.pending;
8440
+ return {
8441
+ ...state,
8442
+ [projectId]: {
8443
+ ...project2,
8444
+ recovery: {
8445
+ ...project2.recovery,
8446
+ [threadId]: {
8447
+ ...member,
8448
+ turnId,
8449
+ status: session.status,
8450
+ retriedItemIds: succeeded ? [] : member.retriedItemIds,
8451
+ ...succeeded ? { unprogressedRetryItemIds: [] } : {},
8452
+ 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,
8453
+ pending
8454
+ }
8455
+ }
8456
+ }
8457
+ };
8458
+ }
8459
+ }
8460
+ if (event.type === "work.item-updated" || event.type === "work.item-claimed" || event.type === "work.comment-posted") {
8461
+ const decoded = decodeWork(event.payload);
8462
+ if (isNone2(decoded)) return state;
8463
+ const work = decoded.value;
8464
+ const project2 = readFact(state, work.projectId);
8465
+ if (project2 === null) return state;
8466
+ const members = Object.entries(project2.recovery ?? {}).filter(
8467
+ ([, member]) => member.pending !== null || member.retriedItemIds.length > 0 || (member.remindedItemIds?.length ?? 0) > 0 || (member.escalatedItemIds?.length ?? 0) > 0 || (member.unprogressedRetryItemIds?.length ?? 0) > 0
8468
+ );
8469
+ if (members.length === 0) return state;
8470
+ const pendingThreads = new Set(
8471
+ members.filter(([, member]) => member.pending !== null).map(([threadId]) => threadId)
8472
+ );
8473
+ const startedThreads = /* @__PURE__ */ new Set();
8474
+ const openWorkThreads = /* @__PURE__ */ new Set();
8475
+ if (pendingThreads.size > 0) {
8476
+ for (const [workItemId, item] of Object.entries(project2.items)) {
8477
+ const pendingAssignees = item.assigneeThreads.filter((id) => pendingThreads.has(id));
8478
+ if (pendingAssignees.length === 0) continue;
8479
+ const workable = isWorkable(project2, workItemId);
8480
+ for (const threadId of pendingAssignees) {
8481
+ if (item.stateGroup === "started") startedThreads.add(threadId);
8482
+ if (workable) openWorkThreads.add(threadId);
8483
+ }
8484
+ }
8485
+ }
8486
+ const before = previous[work.projectId]?.items[work.workItemId];
8487
+ const after = project2.items[work.workItemId];
8488
+ const recovery = { ...project2.recovery };
8489
+ let changed = false;
8490
+ for (const [threadId, member] of members) {
8491
+ 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;
8492
+ const assignedOpen = (id) => isAssignedOpen(project2, id, threadId);
8493
+ const retain = (id) => assignedOpen(id) && !(progress && id === work.workItemId);
8494
+ const remindedItemIds = (member.remindedItemIds ?? []).filter(retain);
8495
+ const escalatedItemIds = (member.escalatedItemIds ?? []).filter(retain);
8496
+ const unprogressedRetryItemIds = (member.unprogressedRetryItemIds ?? []).filter(retain);
8497
+ const retriedItemIds = member.retriedItemIds.filter(assignedOpen);
8498
+ const keepsPending = (member.pending?.kind === "stall" ? startedThreads : openWorkThreads).has(threadId);
8499
+ let pending = keepsPending ? member.pending : null;
8500
+ if (pending?.continuedItemIds !== void 0) {
8501
+ const continuedItemIds = pending.continuedItemIds.filter(retain);
8502
+ if (continuedItemIds.length !== pending.continuedItemIds.length)
8503
+ pending = { ...pending, continuedItemIds };
8504
+ }
8505
+ 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))
8506
+ continue;
8507
+ changed = true;
8508
+ recovery[threadId] = {
8509
+ ...member,
8510
+ retriedItemIds,
8511
+ remindedItemIds,
8512
+ escalatedItemIds,
8513
+ unprogressedRetryItemIds,
8514
+ pending,
8515
+ handledTurnIds: pending === null && member.pending !== null ? [.../* @__PURE__ */ new Set([...member.handledTurnIds, member.pending.turnId])] : member.handledTurnIds
8516
+ };
8517
+ }
8518
+ return changed ? { ...state, [work.projectId]: { ...project2, recovery } } : state;
8519
+ }
8520
+ return state;
8521
+ }
8522
+ function dueRecoveries(state, changedThreadId) {
8523
+ return Object.entries(state).flatMap(
8524
+ ([projectId, project2]) => Object.entries(project2.recovery ?? {}).flatMap(([threadId, member]) => {
8525
+ const memberId = project2.memberIds?.[threadId];
8526
+ const leadThreadId = project2.leadThreads?.[0];
8527
+ 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 }))
8528
+ return [];
8529
+ const stalled = member.pending.kind === "stall";
8530
+ if (member.handledTurnIds.includes(member.pending.turnId)) return [];
8531
+ const items = Object.entries(project2.items).filter(
8532
+ ([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))
8533
+ ).map(([workItemId, item]) => ({
8534
+ workItemId,
8535
+ sequenceId: item.sequenceId,
8536
+ lastCommentId: item.lastCommentId,
8537
+ waitingForReply: item.lastActionCommentThreadId === threadId
8538
+ })).sort((a, b) => a.sequenceId - b.sequenceId);
8539
+ const deferredWakeCount = countDeferredWakes(project2, threadId);
8540
+ return items.length === 0 ? [] : [
8541
+ {
8542
+ ...member.pending,
8543
+ projectId,
8544
+ threadId,
8545
+ memberId,
8546
+ leadThreadId,
8547
+ key: JSON.stringify([threadId, member.pending.turnId]),
8548
+ alreadyRetried: items.some(
8549
+ (item) => member.retriedItemIds.includes(item.workItemId) || (member.remindedItemIds ?? []).includes(item.workItemId) || (member.escalatedItemIds ?? []).includes(item.workItemId) || (member.pending?.continuedItemIds ?? []).includes(item.workItemId)
8550
+ ),
8551
+ items,
8552
+ ...deferredWakeCount > 0 ? { deferredWakeCount } : {}
8553
+ }
8554
+ ];
8555
+ })
8556
+ );
8557
+ }
8558
+
8216
8559
  // src/runtime/wakeState.ts
8217
8560
  var WAKE_STATE_KEY = "wakes";
8218
- var WAKE_STATE_VERSION = 5;
8561
+ var WAKE_STATE_VERSION = 7;
8219
8562
  var WAKE_EVENTS = [
8220
8563
  "work.item-created",
8221
8564
  "work.item-updated",
@@ -8223,7 +8566,8 @@ var WAKE_EVENTS = [
8223
8566
  "work.relation-added",
8224
8567
  "work.relation-removed",
8225
8568
  "work.comment-posted",
8226
- "plugin.event-appended"
8569
+ "plugin.event-appended",
8570
+ "thread.session-set"
8227
8571
  ];
8228
8572
  var MemberWokenFact = Struct({
8229
8573
  version: Literal2(1),
@@ -8245,9 +8589,6 @@ var emptyProject = () => ({
8245
8589
  });
8246
8590
  var itemWakeKey = (workItemId, threadId, generation) => generation === 0 ? `item:${workItemId}:${threadId}` : `item:${workItemId}:${threadId}#${generation}`;
8247
8591
  var unblockedWakeKey = (workItemId, threadId, generation) => `unblocked:${workItemId}:${threadId}#${generation}`;
8248
- var hasOpenBlocker = (project2, workItemId) => (readFact(project2.blockers, workItemId) ?? []).some(
8249
- (blocker) => readFact(project2.items, blocker)?.closed !== true
8250
- );
8251
8592
  var markBlockedAfterClaim = (project2, workItemId) => ({
8252
8593
  ...project2,
8253
8594
  blockedAfterClaim: {
@@ -8279,8 +8620,10 @@ var Actor = Union2([
8279
8620
  Struct({ kind: Literal2("plugin"), pluginId: String4 }),
8280
8621
  Struct({ kind: Literals(["local-cli", "seat", "system"]) })
8281
8622
  ]);
8282
- var StateRef = Struct({ group: String4 });
8283
- var isClosed = (state) => state.group === "completed" || state.group === "cancelled";
8623
+ var StateRef = Struct({
8624
+ group: String4,
8625
+ stateId: optionalKey2(String4)
8626
+ });
8284
8627
  var itemFields = { projectId: String4, workItemId: String4 };
8285
8628
  var relationFields = {
8286
8629
  ...itemFields,
@@ -8337,7 +8680,6 @@ var decodeWorkEvent = decodeUnknownOption2(
8337
8680
  var agentThreads = (principals) => [
8338
8681
  ...new Set(principals.flatMap((p) => p.kind === "agent" ? [p.threadId] : []))
8339
8682
  ];
8340
- var isMember = (project2, recipient) => "threadId" in recipient ? project2.threads.includes(recipient.threadId) : project2.remoteMembers.includes(recipient.principalId);
8341
8683
  var commentRecipients = (project2, actor, people) => {
8342
8684
  const recipients = /* @__PURE__ */ new Map();
8343
8685
  for (const person of people) {
@@ -8349,7 +8691,7 @@ var commentRecipients = (project2, actor, people) => {
8349
8691
  return [...recipients.values()].filter((recipient) => isMember(project2, recipient));
8350
8692
  };
8351
8693
  var commentWakeKey = (commentId, recipient) => "threadId" in recipient ? `comment:${commentId}:${recipient.threadId}` : `comment:${commentId}:principal:${recipient.principalId}`;
8352
- function foldWakes(state, event) {
8694
+ function foldWorkWakes(state, event) {
8353
8695
  if (event.type === "plugin.event-appended") {
8354
8696
  const decoded2 = decodeEnvelope2(event.payload);
8355
8697
  if (isNone2(decoded2)) return state;
@@ -8367,7 +8709,16 @@ function foldWakes(state, event) {
8367
8709
  )
8368
8710
  ];
8369
8711
  const leadThreads = goal.threads.filter((thread) => thread.memberId === goal.leadMemberId).map((thread) => thread.threadId);
8370
- const next2 = { ...project4, threads, leadThreads, remoteMembers };
8712
+ const memberIds = Object.fromEntries(
8713
+ goal.threads.map((member) => [member.threadId, member.memberId])
8714
+ );
8715
+ const next2 = {
8716
+ ...project4,
8717
+ memberIds,
8718
+ threads,
8719
+ leadThreads,
8720
+ remoteMembers
8721
+ };
8371
8722
  return {
8372
8723
  ...state,
8373
8724
  [goal.projectId]: {
@@ -8388,7 +8739,18 @@ function foldWakes(state, event) {
8388
8739
  };
8389
8740
  }
8390
8741
  if (project3.woken.includes(key)) return state;
8391
- return { ...state, [projectId2]: { ...project3, woken: [...project3.woken, key] } };
8742
+ const items = { ...project3.items };
8743
+ for (const [workItemId2, item] of Object.entries(items)) {
8744
+ for (const threadId of item.assigneeThreads) {
8745
+ if (item.wakeStatus?.[threadId] !== "released" && isWorkable(project3, workItemId2) && key === itemWakeKey(
8746
+ workItemId2,
8747
+ threadId,
8748
+ readFact(project3.rearms, `${workItemId2}:${threadId}`) ?? 0
8749
+ ))
8750
+ items[workItemId2] = { ...item, wakeStatus: { ...item.wakeStatus, [threadId]: "active" } };
8751
+ }
8752
+ }
8753
+ return { ...state, [projectId2]: { ...project3, items, woken: [...project3.woken, key] } };
8392
8754
  }
8393
8755
  const decoded = decodeWorkEvent(event);
8394
8756
  if (isNone2(decoded)) return state;
@@ -8406,8 +8768,15 @@ function foldWakes(state, event) {
8406
8768
  [workItemId]: {
8407
8769
  sequenceId: work.payload.sequenceId,
8408
8770
  assigneeThreads: agentThreads(work.payload.assignees),
8771
+ wakeStatus: Object.fromEntries(
8772
+ agentThreads(work.payload.assignees).map((threadId) => [
8773
+ threadId,
8774
+ "pending"
8775
+ ])
8776
+ ),
8409
8777
  claimedBy: [],
8410
- closed: isClosed(work.payload.state)
8778
+ stateGroup: work.payload.state.group,
8779
+ ...work.payload.state.stateId === void 0 ? {} : { stateId: work.payload.state.stateId }
8411
8780
  }
8412
8781
  }
8413
8782
  };
@@ -8421,6 +8790,22 @@ function foldWakes(state, event) {
8421
8790
  ...agentThreads([work.payload.assignee])
8422
8791
  ])
8423
8792
  ];
8793
+ const wakeStatus = { ...currentItem.wakeStatus };
8794
+ for (const threadId of currentItem.assigneeThreads)
8795
+ if (!assigneeThreads.includes(threadId)) wakeStatus[threadId] = "released";
8796
+ for (const threadId of assigneeThreads) {
8797
+ if (!currentItem.assigneeThreads.includes(threadId)) {
8798
+ const generation = readFact(project2.rearms, `${workItemId}:${threadId}`) ?? 0;
8799
+ wakeStatus[threadId] = project2.woken.includes(
8800
+ itemWakeKey(workItemId, threadId, generation)
8801
+ ) ? "released" : "pending";
8802
+ }
8803
+ if (wakeStatus[threadId] === "active" && (isClosed({ stateGroup: work.payload.state?.group ?? currentItem.stateGroup }) || work.payload.state?.group === "unstarted" || work.payload.state?.group === "backlog"))
8804
+ wakeStatus[threadId] = "released";
8805
+ }
8806
+ if (work.type === "work.item-claimed" && hasOpenBlocker(project2, workItemId))
8807
+ for (const threadId of agentThreads([work.payload.assignee]))
8808
+ wakeStatus[threadId] = "released";
8424
8809
  next = {
8425
8810
  ...project2,
8426
8811
  items: {
@@ -8428,8 +8813,11 @@ function foldWakes(state, event) {
8428
8813
  [workItemId]: {
8429
8814
  ...currentItem,
8430
8815
  assigneeThreads,
8816
+ wakeStatus,
8431
8817
  claimedBy: work.type === "work.item-claimed" ? [.../* @__PURE__ */ new Set([...currentItem.claimedBy, ...agentThreads([work.payload.assignee])])] : currentItem.claimedBy,
8432
- closed: work.payload.state === void 0 ? currentItem.closed : isClosed(work.payload.state)
8818
+ ...work.payload.state === void 0 ? {} : { stateGroup: work.payload.state.group },
8819
+ ...work.payload.state?.stateId === void 0 ? {} : { stateId: work.payload.state.stateId },
8820
+ lastActionCommentThreadId: null
8433
8821
  }
8434
8822
  }
8435
8823
  };
@@ -8449,16 +8837,26 @@ function foldWakes(state, event) {
8449
8837
  [workItemId]: work.type === "work.relation-added" ? [.../* @__PURE__ */ new Set([...blockers, related])] : blockers.filter((blocker) => blocker !== related)
8450
8838
  }
8451
8839
  };
8452
- if (work.type === "work.relation-added" && currentItem !== null && readFact(project2.items, related)?.closed !== true) {
8840
+ if (work.type === "work.relation-added" && currentItem !== null && !isClosed(readFact(project2.items, related))) {
8453
8841
  const woken = new Set(project2.woken);
8454
8842
  const rearms = { ...project2.rearms };
8843
+ const wakeStatus = { ...currentItem.wakeStatus };
8455
8844
  for (const threadId of currentItem.assigneeThreads) {
8456
- if (currentItem.claimedBy.includes(threadId)) continue;
8845
+ if (currentItem.claimedBy.includes(threadId)) {
8846
+ wakeStatus[threadId] = "released";
8847
+ continue;
8848
+ }
8457
8849
  const generation = rearms[`${workItemId}:${threadId}`] ?? 0;
8458
- if (woken.has(itemWakeKey(workItemId, threadId, generation)))
8850
+ if (woken.has(itemWakeKey(workItemId, threadId, generation))) {
8459
8851
  rearms[`${workItemId}:${threadId}`] = generation + 1;
8852
+ if (wakeStatus[threadId] !== "released") wakeStatus[threadId] = "pending";
8853
+ }
8460
8854
  }
8461
- next = { ...next, rearms };
8855
+ next = {
8856
+ ...next,
8857
+ rearms,
8858
+ items: { ...next.items, [workItemId]: { ...currentItem, wakeStatus } }
8859
+ };
8462
8860
  if (currentItem.claimedBy.length > 0 && !hasOpenBlocker(project2, workItemId))
8463
8861
  next = markBlockedAfterClaim(next, workItemId);
8464
8862
  }
@@ -8478,45 +8876,104 @@ function foldWakes(state, event) {
8478
8876
  })).filter(
8479
8877
  (comment) => !project2.pendingComments.some((pending) => pending.key === comment.key)
8480
8878
  );
8481
- if (comments.length === 0) return state;
8482
- next = { ...project2, pendingComments: [...project2.pendingComments, ...comments] };
8879
+ next = {
8880
+ ...project2,
8881
+ pendingComments: [...project2.pendingComments, ...comments],
8882
+ items: currentItem === null ? project2.items : {
8883
+ ...project2.items,
8884
+ [workItemId]: {
8885
+ ...currentItem,
8886
+ lastCommentId: commentId,
8887
+ lastActionCommentThreadId: actor.kind === "agent" ? actor.threadId : null
8888
+ }
8889
+ }
8890
+ };
8483
8891
  break;
8484
8892
  }
8485
8893
  }
8486
8894
  return { ...state, [projectId]: next };
8487
8895
  }
8488
- var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
8896
+ function foldWakes(state, event) {
8897
+ return foldRecovery(foldWorkWakes(state, event), event, state);
8898
+ }
8899
+ var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
8900
+ var itemWakeCandidate = (projectId, project2, workItemId, item, threadId, woken) => {
8901
+ if (!project2.threads.includes(threadId) || !isWorkable(project2, workItemId)) return null;
8902
+ const unblocked = readFact(project2.blockedAfterClaim, workItemId) ?? 0;
8903
+ const finishing = unblocked > 0 && item.claimedBy.includes(threadId);
8904
+ if (!finishing && !item.assigneeThreads.includes(threadId)) return null;
8905
+ const lead = project2.leadThreads?.includes(threadId) === true;
8906
+ const key = finishing ? unblockedWakeKey(workItemId, threadId, unblocked) : itemWakeKey(workItemId, threadId, readFact(project2.rearms, `${workItemId}:${threadId}`) ?? 0);
8907
+ if (woken.has(key) || !finishing && !lead && item.wakeStatus?.[threadId] === "released")
8908
+ return null;
8909
+ return {
8910
+ key,
8911
+ projectId,
8912
+ threadId,
8913
+ kind: finishing ? "unblocked" : "item",
8914
+ workItemId,
8915
+ sequenceId: item.sequenceId,
8916
+ // 成员的叫醒话里点名负责人(卡住时找谁);带队自己和解挡续做不带。
8917
+ ...lead ? { lead: true } : finishing || project2.leadThreads?.[0] === void 0 ? {} : { leadThreadId: project2.leadThreads[0] }
8918
+ };
8919
+ };
8920
+ var needsMemberSlot = (wake) => wake.kind === "item" && wake.lead !== true;
8921
+ function memberItemSlots(projectId, project2, woken, onlyThreadId) {
8922
+ const busy = /* @__PURE__ */ new Set();
8923
+ const slots = /* @__PURE__ */ new Map();
8924
+ for (const [workItemId, item] of Object.entries(project2.items)) {
8925
+ if (onlyThreadId !== void 0 && !item.assigneeThreads.includes(onlyThreadId)) continue;
8926
+ if (!isWorkable(project2, workItemId)) continue;
8927
+ for (const threadId of item.assigneeThreads) {
8928
+ if (onlyThreadId !== void 0 && threadId !== onlyThreadId || project2.leadThreads?.includes(threadId) || !project2.threads.includes(threadId))
8929
+ continue;
8930
+ if (item.wakeStatus?.[threadId] === "active") busy.add(threadId);
8931
+ const candidate = itemWakeCandidate(projectId, project2, workItemId, item, threadId, woken);
8932
+ if (candidate === null || !needsMemberSlot(candidate)) continue;
8933
+ const first = slots.get(threadId);
8934
+ if (first === void 0 || compareWakeItems(candidate, first) < 0)
8935
+ slots.set(threadId, candidate);
8936
+ }
8937
+ }
8938
+ for (const threadId of busy) slots.delete(threadId);
8939
+ return slots;
8940
+ }
8941
+ function isWakeDue(state, wake) {
8942
+ const project2 = readFact(state, wake.projectId);
8943
+ if (project2 === null) return false;
8944
+ if (wake.kind === "comment")
8945
+ return project2.pendingComments.some((comment) => comment.key === wake.key);
8946
+ if (!("threadId" in wake)) return false;
8947
+ const item = readFact(project2.items, wake.workItemId);
8948
+ if (item === null) return false;
8949
+ const woken = new Set(project2.woken);
8950
+ const candidate = itemWakeCandidate(
8951
+ wake.projectId,
8952
+ project2,
8953
+ wake.workItemId,
8954
+ item,
8955
+ wake.threadId,
8956
+ woken
8957
+ );
8958
+ if (candidate?.key !== wake.key) return false;
8959
+ return !needsMemberSlot(candidate) || memberItemSlots(wake.projectId, project2, woken, wake.threadId).get(wake.threadId)?.key === wake.key;
8960
+ }
8489
8961
  function dueWakes(state) {
8490
8962
  const due = [];
8491
8963
  for (const [projectId, project2] of Object.entries(state)) {
8492
8964
  const woken = new Set(project2.woken);
8493
- const threads = new Set(project2.threads);
8494
- const leads = new Set(project2.leadThreads ?? []);
8965
+ const slots = memberItemSlots(projectId, project2, woken);
8495
8966
  for (const [workItemId, item] of Object.entries(project2.items)) {
8496
- if (item.closed || hasOpenBlocker(project2, workItemId)) continue;
8497
8967
  const unblocked = readFact(project2.blockedAfterClaim, workItemId) ?? 0;
8498
8968
  const recipients = /* @__PURE__ */ new Set([
8499
8969
  ...item.assigneeThreads,
8500
8970
  ...unblocked > 0 ? item.claimedBy : []
8501
8971
  ]);
8502
8972
  for (const threadId of recipients) {
8503
- if (!threads.has(threadId)) continue;
8504
- const finishing = unblocked > 0 && item.claimedBy.includes(threadId);
8505
- const key = finishing ? unblockedWakeKey(workItemId, threadId, unblocked) : itemWakeKey(
8506
- workItemId,
8507
- threadId,
8508
- readFact(project2.rearms, `${workItemId}:${threadId}`) ?? 0
8509
- );
8510
- if (woken.has(key)) continue;
8511
- due.push({
8512
- key,
8513
- projectId,
8514
- threadId,
8515
- kind: finishing ? "unblocked" : "item",
8516
- workItemId,
8517
- sequenceId: item.sequenceId,
8518
- ...leads.has(threadId) ? { lead: true } : {}
8519
- });
8973
+ const candidate = itemWakeCandidate(projectId, project2, workItemId, item, threadId, woken);
8974
+ if (candidate === null) continue;
8975
+ if (needsMemberSlot(candidate) && slots.get(threadId)?.key !== candidate.key) continue;
8976
+ due.push(candidate);
8520
8977
  }
8521
8978
  }
8522
8979
  for (const comment of project2.pendingComments) {
@@ -8528,13 +8985,26 @@ function dueWakes(state) {
8528
8985
  });
8529
8986
  }
8530
8987
  }
8531
- return due.sort((a, b) => compare(a.projectId, b.projectId) || compare(a.key, b.key));
8988
+ return due.sort((a, b) => compare2(a.projectId, b.projectId) || compare2(a.key, b.key));
8532
8989
  }
8533
8990
 
8534
8991
  // src/runtime/wake.ts
8535
8992
  var workLabel = (workItemId, sequenceId) => sequenceId === void 0 ? `the work item with workItemId ${workItemId}` : `work item W${sequenceId} (workItemId ${workItemId})`;
8536
8993
  var titled = (workItemId, sequenceId, title) => `${workLabel(workItemId, sequenceId)}${title ? `: ${title}` : ""}`;
8537
- var itemText = (workItemId, sequenceId, title) => `You have ${titled(workItemId, sequenceId, title)}. 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. ${handbookMessage("memberBrief")} ${PERSON_MESSAGE_RULE}`;
8994
+ var ITEM_SUMMARY_MAX_CHARS = 300;
8995
+ var descriptionStart = (description) => {
8996
+ const first = description?.trim().split(/\n\s*\n/u)[0]?.trim() ?? "";
8997
+ if (first === "") return "";
8998
+ const points = Array.from(first);
8999
+ const summary = points.length > ITEM_SUMMARY_MAX_CHARS ? `${points.slice(0, ITEM_SUMMARY_MAX_CHARS).join("")}\u2026` : first;
9000
+ return `Its description starts: ${summary}`;
9001
+ };
9002
+ var itemHeadline = (sequenceId, item) => {
9003
+ const title = item?.title ? `${sequenceId === void 0 ? "Work item" : `W${sequenceId}`}: ${item.title}.` : "";
9004
+ const parts = [title, descriptionStart(item?.description)].filter((part) => part !== "");
9005
+ return parts.length === 0 ? "" : `${parts.join(" ")} `;
9006
+ };
9007
+ 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}`;
8538
9008
  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}`;
8539
9009
  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}`;
8540
9010
  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.`;
@@ -8549,13 +9019,14 @@ function startWakeRuntime(ctx, options = {}) {
8549
9019
  });
8550
9020
  const hash2 = options.hash ?? stableHash;
8551
9021
  let pending = Promise.resolve();
8552
- const readTitle = async (projectId, workItemId) => {
9022
+ const readItem = async (projectId, workItemId) => {
8553
9023
  const result2 = await ctx.work.items.get({ projectId, workItemIds: [workItemId] });
8554
- return result2.items.find((item) => item.workItemId === workItemId)?.title;
9024
+ return result2.items.find((item) => item.workItemId === workItemId);
8555
9025
  };
8556
9026
  const execute = async () => {
8557
- const state = await wakes.get();
9027
+ let state = await wakes.get();
8558
9028
  for (const wake of dueWakes(state)) {
9029
+ if (!isWakeDue(state, wake)) continue;
8559
9030
  try {
8560
9031
  const id = hash2(wake.key);
8561
9032
  const delivered = await deliverWake(ctx, {
@@ -8563,11 +9034,19 @@ function startWakeRuntime(ctx, options = {}) {
8563
9034
  recipient: wake,
8564
9035
  requestId: `team-wake:${id}`,
8565
9036
  messageId: `team-wake-msg:${id}`,
8566
- text: async () => "principalId" in wake || wake.kind === "comment" ? commentText2(wake.workItemId, wake.sequenceId) : (wake.kind === "item" ? wake.lead === true ? leadItemText : itemText : wake.lead === true ? leadUnblockedText : unblockedText)(
8567
- wake.workItemId,
8568
- wake.sequenceId,
8569
- await readTitle(wake.projectId, wake.workItemId)
8570
- )
9037
+ text: async () => {
9038
+ if ("principalId" in wake || wake.kind === "comment")
9039
+ return commentText2(wake.workItemId, wake.sequenceId);
9040
+ const item = await readItem(wake.projectId, wake.workItemId);
9041
+ const title = item?.title;
9042
+ if (wake.kind === "item")
9043
+ return wake.lead === true ? leadItemText(wake.workItemId, wake.sequenceId, title) : itemText(wake.workItemId, wake.sequenceId, item, wake.leadThreadId);
9044
+ return (wake.lead === true ? leadUnblockedText : unblockedText)(
9045
+ wake.workItemId,
9046
+ wake.sequenceId,
9047
+ title
9048
+ );
9049
+ }
8571
9050
  });
8572
9051
  if (!delivered) continue;
8573
9052
  const receipt = await ctx.events.append({
@@ -8581,7 +9060,7 @@ function startWakeRuntime(ctx, options = {}) {
8581
9060
  ..."principalId" in wake ? { principalId: wake.principalId } : { threadId: wake.threadId }
8582
9061
  }
8583
9062
  });
8584
- await wakes.get({ throughSequence: receipt.sequence });
9063
+ state = await wakes.get({ throughSequence: receipt.sequence });
8585
9064
  } catch (error) {
8586
9065
  ctx.log?.warn("team member wake failed", { key: wake.key, error });
8587
9066
  }
@@ -8595,10 +9074,212 @@ function startWakeRuntime(ctx, options = {}) {
8595
9074
  );
8596
9075
  return result2;
8597
9076
  };
8598
- ctx.reactor({ name: "team-wake", events: WAKE_EVENTS, handle: () => flush() });
9077
+ ctx.reactor({
9078
+ name: "team-wake",
9079
+ events: WAKE_EVENTS,
9080
+ handle: (event) => event.type === "thread.session-set" || isRecoveryFact(event) ? Promise.resolve() : flush()
9081
+ });
8599
9082
  const ready = flush();
8600
9083
  void ready.catch((error) => ctx.log?.warn("team wake catch-up failed", { error }));
8601
- return { flush, ready };
9084
+ return { flush, ready, state: wakes };
9085
+ }
9086
+
9087
+ // src/runtime/recovery.ts
9088
+ var readStallExecution = async (ctx, threadId) => {
9089
+ const shell = await ctx.threads.get({ threadId });
9090
+ const summary = await ctx.threads.execution.summary({ threadId, limit: 1 });
9091
+ return { ...shell, queuePaused: summary.queuePaused, summary };
9092
+ };
9093
+ var itemLabel = (item) => `W${item.sequenceId} (workItemId ${item.workItemId})`;
9094
+ 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. `;
9095
+ 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)}`;
9096
+ var commentInstructions = (recovery) => recovery.items.map(
9097
+ (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.`
9098
+ ).join("\n");
9099
+ 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." : ""}`;
9100
+ var resumeText = (recovery) => `Your previous turn was interrupted by an upstream error (${recovery.category}). Continue ${recovery.items.map(itemLabel).join(", ")} from where you stopped.
9101
+ ${commentInstructions(recovery)}`;
9102
+ 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.
9103
+ ${commentInstructions(recovery)}`;
9104
+ 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(
9105
+ (item) => `Their last action on ${itemLabel(item)} was their own comment (commentId ${item.lastCommentId}); reply to it or arrange another member to help. `
9106
+ ).join("")}${leadInstructions()}`;
9107
+ function startRecoveryRuntime(ctx, goals, state) {
9108
+ let pending = Promise.resolve();
9109
+ let disposed = false;
9110
+ const execute = async (scope2) => {
9111
+ if (disposed) return;
9112
+ const current = await state.get();
9113
+ let projects = current;
9114
+ if (scope2?.kind === "project") {
9115
+ const project2 = readFact(current, scope2.projectId);
9116
+ projects = project2 === null ? {} : { [scope2.projectId]: project2 };
9117
+ } else if (scope2?.kind === "thread") {
9118
+ projects = Object.fromEntries(
9119
+ Object.entries(current).filter(
9120
+ ([, project2]) => Object.entries(project2.recovery ?? {}).some(
9121
+ ([threadId, member]) => threadId === scope2.threadId || member.pending?.plan?.recipientThreadId === scope2.threadId
9122
+ )
9123
+ )
9124
+ );
9125
+ }
9126
+ const recoveries = dueRecoveries(
9127
+ projects,
9128
+ scope2?.kind === "thread" ? scope2.threadId : void 0
9129
+ );
9130
+ const wakingThreads = new Set(
9131
+ recoveries.some((recovery) => recovery.kind === "stall") ? dueWakes(projects).flatMap((wake) => "threadId" in wake ? [wake.threadId] : []) : []
9132
+ );
9133
+ for (const recovery of recoveries) {
9134
+ try {
9135
+ const stalled = recovery.kind === "stall";
9136
+ const execution = stalled ? await readStallExecution(ctx, recovery.threadId) : await ctx.projections.getThreadExecution(recovery.threadId);
9137
+ const queuedContinuation = recovery.plan?.resumeQueue === true;
9138
+ 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")))
9139
+ continue;
9140
+ if ("summary" in execution) {
9141
+ if (execution.summary.queuePaused || execution.summary.pendingApprovalCount > 0 || execution.summary.pendingUserInputCount > 0 || execution.summary.queue.length > 0 || (execution.pendingAsyncQuestionCount ?? 0) > 0 || // 成员给人发了消息、人还没回:他在等回答,不提醒也不叫负责人。
9142
+ (execution.awaitingPersonReplyCount ?? 0) > 0)
9143
+ continue;
9144
+ if (wakingThreads.has(recovery.threadId) || Object.values(projects).some(
9145
+ (project2) => Object.values(project2.recovery ?? {}).some(
9146
+ (member) => member.pending?.plan?.recipientThreadId === recovery.threadId && member.pending !== current[recovery.projectId]?.recovery?.[recovery.threadId]?.pending
9147
+ )
9148
+ ))
9149
+ continue;
9150
+ }
9151
+ const id = stableHash(recovery.key);
9152
+ let plan = recovery.plan;
9153
+ let tryQueueResume = false;
9154
+ const requestPlan = async (next) => {
9155
+ const receipt2 = await ctx.events.append({
9156
+ commandId: `team-recovery-requested:${id}:${next.action}${next.resumeQueue === true ? "-queue" : ""}`,
9157
+ name: "member-recovery-requested",
9158
+ aggregate: { kind: "project", id: recovery.projectId },
9159
+ data: {
9160
+ version: 1,
9161
+ projectId: recovery.projectId,
9162
+ threadId: recovery.threadId,
9163
+ turnId: recovery.turnId,
9164
+ failureSequence: recovery.sequence,
9165
+ plan: next
9166
+ }
9167
+ });
9168
+ await state.get({ throughSequence: receipt2.sequence });
9169
+ };
9170
+ if (plan === void 0 || plan.action === "resume" && execution.queuePaused && plan.resumeQueue !== true) {
9171
+ const resume = !stalled && (!recovery.alreadyRetried || plan?.action === "resume") && (recovery.category === "transport" || recovery.category === "rate-limit" && !execution.queuePaused);
9172
+ const remind = stalled && !recovery.alreadyRetried && !recovery.items.some((item) => item.waitingForReply);
9173
+ const goal = resume || remind ? null : await goals.read(recovery.projectId);
9174
+ const name = goal?.members.find((member) => member.threadId === recovery.threadId)?.name ?? recovery.memberId;
9175
+ plan = {
9176
+ action: stalled ? remind ? "remind" : "stall-lead" : resume ? "resume" : "lead",
9177
+ recipientThreadId: resume || remind ? recovery.threadId : recovery.leadThreadId,
9178
+ text: stalled ? remind ? remindText(recovery) : stallLeadText(recovery, name) : resume ? resumeText(recovery) : leadText(recovery, name, execution.queuePaused),
9179
+ workItemIds: recovery.items.map((item) => item.workItemId),
9180
+ ...resume && execution.queuePaused ? { resumeQueue: true } : {}
9181
+ };
9182
+ await requestPlan(plan);
9183
+ tryQueueResume = plan.resumeQueue === true;
9184
+ }
9185
+ if (plan.resumeQueue === true && execution.queuePaused) {
9186
+ const project2 = (await state.get())[recovery.projectId];
9187
+ if (project2 === void 0 || !isMember(project2, { threadId: recovery.threadId }) || project2.leadThreads?.includes(recovery.threadId) || project2.recovery?.[recovery.threadId]?.pending?.turnId !== recovery.turnId)
9188
+ continue;
9189
+ let resumed = false;
9190
+ let outcome = "already-attempted";
9191
+ if (tryQueueResume) {
9192
+ try {
9193
+ const receipt2 = await ctx.threads.turnQueue.resume({
9194
+ requestId: `team-recovery-queue:${id}`,
9195
+ threadId: recovery.threadId
9196
+ });
9197
+ resumed = receipt2.status === "accepted";
9198
+ outcome = "noop";
9199
+ } catch {
9200
+ outcome = "rejected";
9201
+ }
9202
+ }
9203
+ if (!resumed) {
9204
+ const latest = await ctx.projections.getThreadExecution(recovery.threadId).catch(() => null);
9205
+ resumed = latest?.queuePaused === false;
9206
+ ctx.log.warn("team member queue recovery refused", {
9207
+ threadId: recovery.threadId,
9208
+ turnId: recovery.turnId,
9209
+ outcome,
9210
+ queuePaused: !resumed
9211
+ });
9212
+ }
9213
+ if (!resumed) {
9214
+ const goal = await goals.read(recovery.projectId);
9215
+ const name = goal?.members.find((member) => member.threadId === recovery.threadId)?.name ?? recovery.memberId;
9216
+ plan = {
9217
+ action: "lead",
9218
+ recipientThreadId: recovery.leadThreadId,
9219
+ text: leadText(recovery, name, true),
9220
+ workItemIds: plan.workItemIds
9221
+ };
9222
+ await requestPlan(plan);
9223
+ }
9224
+ }
9225
+ const delivered = await deliverWake(ctx, {
9226
+ projectId: recovery.projectId,
9227
+ recipient: { threadId: plan.recipientThreadId },
9228
+ requestId: `team-recovery:${id}:${plan.action}`,
9229
+ messageId: `team-recovery-msg:${id}:${plan.action}`,
9230
+ text: plan.text
9231
+ });
9232
+ if (!delivered) continue;
9233
+ const receipt = await ctx.events.append({
9234
+ commandId: `team-recovered:${id}:${plan.action}`,
9235
+ name: "member-recovery-delivered",
9236
+ aggregate: { kind: "project", id: recovery.projectId },
9237
+ data: {
9238
+ version: 1,
9239
+ projectId: recovery.projectId,
9240
+ threadId: recovery.threadId,
9241
+ turnId: recovery.turnId,
9242
+ failureSequence: recovery.sequence,
9243
+ action: plan.action,
9244
+ workItemIds: plan.workItemIds
9245
+ }
9246
+ });
9247
+ await state.get({ throughSequence: receipt.sequence });
9248
+ } catch (error) {
9249
+ ctx.log.warn("team member recovery failed", { key: recovery.key, error });
9250
+ }
9251
+ }
9252
+ };
9253
+ const flush = (scope2) => {
9254
+ const result2 = pending.then(() => execute(scope2));
9255
+ pending = result2.catch(() => void 0);
9256
+ return result2;
9257
+ };
9258
+ ctx.reactor({
9259
+ name: "team-member-recovery",
9260
+ events: [
9261
+ "thread.session-set",
9262
+ "thread.turn-queue-paused",
9263
+ "plugin.event-appended",
9264
+ "work.item-updated",
9265
+ "work.item-claimed",
9266
+ "work.comment-posted",
9267
+ "work.relation-added",
9268
+ "work.relation-removed",
9269
+ "thread.turn-intent-queued",
9270
+ "thread.turn-intent-transitioned",
9271
+ "thread.turn-queue-updated",
9272
+ "thread.request-resolved",
9273
+ "thread.user-input-pending"
9274
+ ],
9275
+ handle: (event) => flush(recoveryEventScopeOf(event))
9276
+ });
9277
+ ctx.onDispose(() => {
9278
+ disposed = true;
9279
+ });
9280
+ const ready = flush();
9281
+ void ready.catch((error) => ctx.log.warn("team member recovery catch-up failed", { error }));
9282
+ return { ready, flush };
8602
9283
  }
8603
9284
 
8604
9285
  // src/handbook/content.ts
@@ -9304,7 +9985,7 @@ function registerHandbookPanel(ctx, runtime) {
9304
9985
  // src/index.ts
9305
9986
  var index_default = definePlugin({
9306
9987
  id: "team",
9307
- apiVersion: 27,
9988
+ apiVersion: 29,
9308
9989
  activate(ctx) {
9309
9990
  const rosters = createRosterService(ctx);
9310
9991
  registerRosterCommands(ctx, rosters);
@@ -9312,7 +9993,8 @@ var index_default = definePlugin({
9312
9993
  registerGoalCommands(ctx, goals);
9313
9994
  registerBoardCommands(ctx, goals, rosters);
9314
9995
  registerDetailCommands(ctx, goals);
9315
- startWakeRuntime(ctx);
9996
+ const wakes = startWakeRuntime(ctx);
9997
+ startRecoveryRuntime(ctx, goals, wakes.state);
9316
9998
  const handbook = startHandbookRuntime(ctx);
9317
9999
  registerHandbook(ctx, handbook);
9318
10000
  registerHandbookPanel(ctx, handbook);