@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/assets/ui.js +1 -1
- package/dist/index.js +880 -61
- package/package.json +4 -2
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(
|
|
1186
|
-
return (self, that) => self === that ? 0 :
|
|
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>" }].
|
|
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 (!
|
|
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
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
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
|
|
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 =
|
|
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({
|
|
8292
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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))
|
|
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 = {
|
|
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
|
-
|
|
8491
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
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) =>
|
|
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 `
|
|
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) =>
|
|
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
|
-
|
|
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({
|
|
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:
|
|
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
|
-
|
|
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
|
}
|