@schoolai/shipyard 3.2.0 → 3.2.1-rc.20260421.0
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/{chunk-FD2QK7IN.js → chunk-DKMDBOFU.js} +35 -2
- package/dist/chunk-DKMDBOFU.js.map +1 -0
- package/dist/{chunk-5SJBSLGT.js → chunk-FMMRZTOF.js} +2 -2
- package/dist/{chunk-5SJBSLGT.js.map → chunk-FMMRZTOF.js.map} +1 -1
- package/dist/{chunk-CANDZLMB.js → chunk-JSUYB74F.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/{login-GRM7QIPC.js → login-KSI4GTLM.js} +3 -3
- package/dist/{roi-3Y7MWLSW.js → roi-YQ6OLVHX.js} +8 -5
- package/dist/roi-YQ6OLVHX.js.map +1 -0
- package/dist/{serve-3EFFP3PN.js → serve-P5WC5JIT.js} +1478 -222
- package/dist/{serve-3EFFP3PN.js.map → serve-P5WC5JIT.js.map} +1 -1
- package/dist/{start-24JXLIQJ.js → start-2K7HFXHV.js} +4 -4
- package/package.json +1 -1
- package/dist/chunk-FD2QK7IN.js.map +0 -1
- package/dist/roi-3Y7MWLSW.js.map +0 -1
- /package/dist/{chunk-CANDZLMB.js.map → chunk-JSUYB74F.js.map} +0 -0
- /package/dist/{login-GRM7QIPC.js.map → login-KSI4GTLM.js.map} +0 -0
- /package/dist/{start-24JXLIQJ.js.map → start-2K7HFXHV.js.map} +0 -0
|
@@ -4,10 +4,11 @@ import {
|
|
|
4
4
|
appendTrailerToMessage,
|
|
5
5
|
emitCommitAttributed,
|
|
6
6
|
emitPrAttributed,
|
|
7
|
+
emitPrMerged,
|
|
7
8
|
emitTaskEnded,
|
|
8
9
|
emitTaskStarted,
|
|
9
10
|
hasShipyardTrailer
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-DKMDBOFU.js";
|
|
11
12
|
import {
|
|
12
13
|
AuthGitHubCallbackRequestSchema,
|
|
13
14
|
AuthGitHubCallbackResponseSchema,
|
|
@@ -46,7 +47,7 @@ import {
|
|
|
46
47
|
VaultKeyPutRequestSchema,
|
|
47
48
|
VaultKeyPutResponseSchema,
|
|
48
49
|
classifyClaudeCodeCompatibility
|
|
49
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-FMMRZTOF.js";
|
|
50
51
|
import "./chunk-EHQITHQX.js";
|
|
51
52
|
import {
|
|
52
53
|
loadAuthToken
|
|
@@ -82,7 +83,7 @@ import {
|
|
|
82
83
|
} from "./chunk-2H7UOFLK.js";
|
|
83
84
|
|
|
84
85
|
// src/services/serve.ts
|
|
85
|
-
import { mkdir as mkdir24 } from "fs/promises";
|
|
86
|
+
import { mkdir as mkdir24, realpath as realpath2 } from "fs/promises";
|
|
86
87
|
import { join as join55 } from "path";
|
|
87
88
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
88
89
|
|
|
@@ -27123,7 +27124,10 @@ var MessageSchema = external_exports.object({
|
|
|
27123
27124
|
reasoningEffort: ReasoningEffortSchema.nullable().optional(),
|
|
27124
27125
|
permissionMode: PermissionModeSchema2.nullable().optional(),
|
|
27125
27126
|
isSynthetic: external_exports.boolean().optional(),
|
|
27126
|
-
anchorToolUseId: external_exports.string().optional()
|
|
27127
|
+
anchorToolUseId: external_exports.string().optional(),
|
|
27128
|
+
correlationId: external_exports.string().optional(),
|
|
27129
|
+
batchCorrelationIds: external_exports.array(external_exports.string()).optional(),
|
|
27130
|
+
sdkUuid: external_exports.string().optional()
|
|
27127
27131
|
});
|
|
27128
27132
|
var ChannelSchema = external_exports.object({
|
|
27129
27133
|
id: external_exports.string(),
|
|
@@ -27286,9 +27290,14 @@ var TaskRecordSchema = external_exports.object({
|
|
|
27286
27290
|
abandonedAt: external_exports.number().nullable().optional(),
|
|
27287
27291
|
lastPlanDetection: PlanDetectionSchema.optional(),
|
|
27288
27292
|
lastActivityAt: external_exports.number().default(0),
|
|
27289
|
-
appliedTemplateId: external_exports.string().optional()
|
|
27293
|
+
appliedTemplateId: external_exports.string().optional(),
|
|
27294
|
+
roiStartedEmitted: external_exports.boolean().default(false),
|
|
27295
|
+
totalTurnCount: external_exports.number().int().nonnegative().default(0),
|
|
27296
|
+
mergedAt: external_exports.number().int().positive().nullable().default(null),
|
|
27297
|
+
attributedCommitShas: external_exports.array(external_exports.string()).max(50).default([]),
|
|
27298
|
+
lastCommitScanSha: external_exports.string().nullable().default(null)
|
|
27290
27299
|
});
|
|
27291
|
-
var TASK_STORE_VERSION =
|
|
27300
|
+
var TASK_STORE_VERSION = 11;
|
|
27292
27301
|
var TaskStoreSchema = external_exports.object({
|
|
27293
27302
|
schemaVersion: external_exports.number(),
|
|
27294
27303
|
tasks: external_exports.record(external_exports.string(), TaskRecordSchema)
|
|
@@ -27305,6 +27314,9 @@ function migrateTaskStore(raw) {
|
|
|
27305
27314
|
if (version < 10) {
|
|
27306
27315
|
backfillLastActivityAt(prop(raw, "tasks"));
|
|
27307
27316
|
}
|
|
27317
|
+
if (version < 11) {
|
|
27318
|
+
backfillRoiStartedEmitted(prop(raw, "tasks"));
|
|
27319
|
+
}
|
|
27308
27320
|
return TaskStoreSchema.parse({ ...raw, schemaVersion: TASK_STORE_VERSION });
|
|
27309
27321
|
}
|
|
27310
27322
|
function backfillLastActivityAt(tasksRaw) {
|
|
@@ -27322,6 +27334,23 @@ function backfillLastActivityAt(tasksRaw) {
|
|
|
27322
27334
|
Object.assign(record, { lastActivityAt: fallback });
|
|
27323
27335
|
}
|
|
27324
27336
|
}
|
|
27337
|
+
function backfillRoiStartedEmitted(tasksRaw) {
|
|
27338
|
+
if (typeof tasksRaw !== "object" || tasksRaw === null)
|
|
27339
|
+
return;
|
|
27340
|
+
const alreadyRanStatuses = /* @__PURE__ */ new Set(["in_progress", "input_required", "completed", "canceled"]);
|
|
27341
|
+
for (const record of Object.values(tasksRaw)) {
|
|
27342
|
+
if (typeof record !== "object" || record === null)
|
|
27343
|
+
continue;
|
|
27344
|
+
const existing = prop(record, "roiStartedEmitted");
|
|
27345
|
+
if (existing === true)
|
|
27346
|
+
continue;
|
|
27347
|
+
const taskStartedAt = prop(record, "taskStartedAt");
|
|
27348
|
+
const status = prop(record, "status");
|
|
27349
|
+
if (taskStartedAt != null || typeof status === "string" && alreadyRanStatuses.has(status)) {
|
|
27350
|
+
Object.assign(record, { roiStartedEmitted: true });
|
|
27351
|
+
}
|
|
27352
|
+
}
|
|
27353
|
+
}
|
|
27325
27354
|
var TemplateItemSchema = external_exports.object({
|
|
27326
27355
|
id: external_exports.string(),
|
|
27327
27356
|
content: external_exports.string(),
|
|
@@ -27584,6 +27613,7 @@ var CheckpointFileEntrySchema = external_exports.object({
|
|
|
27584
27613
|
var BrowserToDaemonMessageSchema = external_exports.discriminatedUnion("type", [
|
|
27585
27614
|
external_exports.object({
|
|
27586
27615
|
type: external_exports.literal("send_message"),
|
|
27616
|
+
correlationId: external_exports.string(),
|
|
27587
27617
|
content: external_exports.array(ContentBlockSchema),
|
|
27588
27618
|
model: external_exports.string().optional(),
|
|
27589
27619
|
reasoningEffort: ReasoningEffortSchema.optional(),
|
|
@@ -27595,7 +27625,8 @@ var BrowserToDaemonMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
27595
27625
|
external_exports.object({
|
|
27596
27626
|
type: external_exports.literal("subscribe"),
|
|
27597
27627
|
sinceSeqNo: external_exports.number(),
|
|
27598
|
-
maxMessages: external_exports.number().int().positive().optional()
|
|
27628
|
+
maxMessages: external_exports.number().int().positive().optional(),
|
|
27629
|
+
inFlightCorrelationIds: external_exports.array(external_exports.string()).optional()
|
|
27599
27630
|
}),
|
|
27600
27631
|
external_exports.object({ type: external_exports.literal("cancel_queued") }),
|
|
27601
27632
|
external_exports.object({
|
|
@@ -27662,6 +27693,19 @@ var DaemonToBrowserMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
27662
27693
|
type: external_exports.literal("older_messages"),
|
|
27663
27694
|
messages: external_exports.array(MessageSchema),
|
|
27664
27695
|
hasMore: external_exports.boolean()
|
|
27696
|
+
}),
|
|
27697
|
+
external_exports.object({
|
|
27698
|
+
type: external_exports.literal("send_message_ack"),
|
|
27699
|
+
correlationId: external_exports.string(),
|
|
27700
|
+
stage: external_exports.enum(["accepted", "persisted", "forwarded", "confirmed", "rejected"]),
|
|
27701
|
+
error: external_exports.string().optional()
|
|
27702
|
+
}),
|
|
27703
|
+
external_exports.object({
|
|
27704
|
+
type: external_exports.literal("correlation_status_snapshot"),
|
|
27705
|
+
entries: external_exports.array(external_exports.object({
|
|
27706
|
+
correlationId: external_exports.string(),
|
|
27707
|
+
status: external_exports.enum(["unknown", "persisted", "forwarded", "confirmed", "rejected"])
|
|
27708
|
+
}))
|
|
27665
27709
|
})
|
|
27666
27710
|
]);
|
|
27667
27711
|
var PermissionRequestPayloadSchema = external_exports.object({
|
|
@@ -27757,7 +27801,8 @@ var BrowserToDaemonControlMessageSchema = external_exports.discriminatedUnion("t
|
|
|
27757
27801
|
}),
|
|
27758
27802
|
external_exports.object({
|
|
27759
27803
|
type: external_exports.literal("update_settings"),
|
|
27760
|
-
settings: DaemonSettingsSchema
|
|
27804
|
+
settings: DaemonSettingsSchema,
|
|
27805
|
+
correlationId: external_exports.string().optional()
|
|
27761
27806
|
}),
|
|
27762
27807
|
external_exports.object({
|
|
27763
27808
|
type: external_exports.literal("update_task_settings"),
|
|
@@ -28067,7 +28112,8 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
|
|
|
28067
28112
|
external_exports.object({
|
|
28068
28113
|
type: external_exports.literal("settings_ack"),
|
|
28069
28114
|
settings: DaemonSettingsSchema,
|
|
28070
|
-
taskId: external_exports.string().optional()
|
|
28115
|
+
taskId: external_exports.string().optional(),
|
|
28116
|
+
correlationId: external_exports.string().optional()
|
|
28071
28117
|
}),
|
|
28072
28118
|
external_exports.object({
|
|
28073
28119
|
type: external_exports.literal("enhance_prompt_chunk"),
|
|
@@ -28375,6 +28421,15 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
|
|
|
28375
28421
|
external_exports.object({ type: external_exports.literal("template_list"), templates: external_exports.array(TaskTemplateRecordSchema) }),
|
|
28376
28422
|
external_exports.object({ type: external_exports.literal("template_updated"), template: TaskTemplateRecordSchema }),
|
|
28377
28423
|
external_exports.object({ type: external_exports.literal("template_deleted"), templateId: external_exports.string() }),
|
|
28424
|
+
external_exports.object({
|
|
28425
|
+
type: external_exports.literal("task_created_ack"),
|
|
28426
|
+
taskId: external_exports.string(),
|
|
28427
|
+
templateId: external_exports.string().nullable(),
|
|
28428
|
+
appliedTemplate: external_exports.object({
|
|
28429
|
+
templateId: external_exports.string(),
|
|
28430
|
+
todos: external_exports.array(external_exports.object({ id: external_exports.string(), content: external_exports.string(), deps: external_exports.array(external_exports.string()) }))
|
|
28431
|
+
}).nullable()
|
|
28432
|
+
}),
|
|
28378
28433
|
external_exports.object({
|
|
28379
28434
|
type: external_exports.literal("review_comment"),
|
|
28380
28435
|
taskId: external_exports.string(),
|
|
@@ -28684,7 +28739,11 @@ var DaemonToFileIOMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
28684
28739
|
}),
|
|
28685
28740
|
external_exports.object({ type: external_exports.literal("git_stage_result"), requestId: external_exports.string() }),
|
|
28686
28741
|
external_exports.object({ type: external_exports.literal("git_unstage_result"), requestId: external_exports.string() }),
|
|
28687
|
-
external_exports.object({
|
|
28742
|
+
external_exports.object({
|
|
28743
|
+
type: external_exports.literal("set_cwd_ack"),
|
|
28744
|
+
requestId: external_exports.string(),
|
|
28745
|
+
canonical: external_exports.string().optional()
|
|
28746
|
+
}),
|
|
28688
28747
|
external_exports.object({
|
|
28689
28748
|
type: external_exports.literal("git_diff_turn_result"),
|
|
28690
28749
|
requestId: external_exports.string(),
|
|
@@ -31391,7 +31450,7 @@ function nanoid(size2 = 21) {
|
|
|
31391
31450
|
}
|
|
31392
31451
|
|
|
31393
31452
|
// src/services/bootstrap/signaling.ts
|
|
31394
|
-
var DAEMON_NPM_VERSION = true ? "3.2.
|
|
31453
|
+
var DAEMON_NPM_VERSION = true ? "3.2.1" : "unknown";
|
|
31395
31454
|
function createDaemonSignaling(config2) {
|
|
31396
31455
|
const agentId = config2.agentId ?? nanoid();
|
|
31397
31456
|
function send(msg) {
|
|
@@ -35103,7 +35162,10 @@ async function rehydrateFromPersistence(persistence, taskManager, log, taskState
|
|
|
35103
35162
|
initialTurnStats: record?.lastTurnStats,
|
|
35104
35163
|
initialTokenCount: record?.lastTokenCount,
|
|
35105
35164
|
initialPlanDetection: record?.lastPlanDetection,
|
|
35106
|
-
mode: record?.mode
|
|
35165
|
+
mode: record?.mode,
|
|
35166
|
+
initialRoiStartedEmitted: record?.roiStartedEmitted ?? false,
|
|
35167
|
+
taskCreatedAt: record?.createdAt ?? Date.now(),
|
|
35168
|
+
initialTurnCount: record?.totalTurnCount ?? 0
|
|
35107
35169
|
});
|
|
35108
35170
|
log({ event: "task_restored", taskId: action.taskId, kind: "resumable" });
|
|
35109
35171
|
break;
|
|
@@ -35120,6 +35182,50 @@ async function rehydrateFromPersistence(persistence, taskManager, log, taskState
|
|
|
35120
35182
|
}
|
|
35121
35183
|
}
|
|
35122
35184
|
}
|
|
35185
|
+
async function applyMainQueueRehydrate(taskId, orchestrator, log) {
|
|
35186
|
+
let claimedByCollab = /* @__PURE__ */ new Set();
|
|
35187
|
+
try {
|
|
35188
|
+
claimedByCollab = await orchestrator.rehydrateCollabQueue();
|
|
35189
|
+
log({ event: "collab_queue_rehydrate_applied", taskId });
|
|
35190
|
+
} catch (err) {
|
|
35191
|
+
log({
|
|
35192
|
+
event: "collab_queue_rehydrate_failed",
|
|
35193
|
+
taskId,
|
|
35194
|
+
error: err instanceof Error ? err.message : String(err)
|
|
35195
|
+
});
|
|
35196
|
+
}
|
|
35197
|
+
try {
|
|
35198
|
+
const count = await orchestrator.rehydrateUnpushedMessages(claimedByCollab);
|
|
35199
|
+
if (count > 0) {
|
|
35200
|
+
log({ event: "unpushed_messages_rehydrate_applied", taskId, count });
|
|
35201
|
+
}
|
|
35202
|
+
} catch (err) {
|
|
35203
|
+
log({
|
|
35204
|
+
event: "unpushed_messages_rehydrate_failed",
|
|
35205
|
+
taskId,
|
|
35206
|
+
error: err instanceof Error ? err.message : String(err)
|
|
35207
|
+
});
|
|
35208
|
+
}
|
|
35209
|
+
}
|
|
35210
|
+
async function applyThreadQueueRehydrate(key, taskId, threadId, orchestrator, persistence, log) {
|
|
35211
|
+
try {
|
|
35212
|
+
const found2 = await orchestrator.rehydrateThreadQueue(threadId);
|
|
35213
|
+
if (found2) {
|
|
35214
|
+
log({ event: "collab_queue_thread_rehydrate_applied", taskId, threadId });
|
|
35215
|
+
} else {
|
|
35216
|
+
await persistence.clear(key).catch(() => {
|
|
35217
|
+
});
|
|
35218
|
+
log({ event: "collab_queue_thread_rehydrate_orphan_cleared", taskId, threadId });
|
|
35219
|
+
}
|
|
35220
|
+
} catch (err) {
|
|
35221
|
+
log({
|
|
35222
|
+
event: "collab_queue_thread_rehydrate_failed",
|
|
35223
|
+
taskId,
|
|
35224
|
+
threadId,
|
|
35225
|
+
error: err instanceof Error ? err.message : String(err)
|
|
35226
|
+
});
|
|
35227
|
+
}
|
|
35228
|
+
}
|
|
35123
35229
|
async function rehydrateCollabQueues(persistence, taskManager, log) {
|
|
35124
35230
|
await persistence.cleanupOrphanTmpFiles().catch(() => {
|
|
35125
35231
|
});
|
|
@@ -35127,12 +35233,6 @@ async function rehydrateCollabQueues(persistence, taskManager, log) {
|
|
|
35127
35233
|
if (keys3.length === 0) return;
|
|
35128
35234
|
for (const key of keys3) {
|
|
35129
35235
|
const parsed = parseQueueKey(key);
|
|
35130
|
-
if (parsed.kind !== "main") {
|
|
35131
|
-
await persistence.clear(key).catch(() => {
|
|
35132
|
-
});
|
|
35133
|
-
log({ event: "collab_queue_thread_rehydrate_skipped", key });
|
|
35134
|
-
continue;
|
|
35135
|
-
}
|
|
35136
35236
|
const orchestrator = taskManager.getOrchestrator(parsed.taskId);
|
|
35137
35237
|
if (!orchestrator) {
|
|
35138
35238
|
await persistence.clear(key).catch(() => {
|
|
@@ -35140,15 +35240,17 @@ async function rehydrateCollabQueues(persistence, taskManager, log) {
|
|
|
35140
35240
|
log({ event: "collab_queue_rehydrate_skipped", taskId: parsed.taskId, reason: "no_task" });
|
|
35141
35241
|
continue;
|
|
35142
35242
|
}
|
|
35143
|
-
|
|
35144
|
-
await orchestrator
|
|
35145
|
-
|
|
35146
|
-
|
|
35147
|
-
|
|
35148
|
-
|
|
35149
|
-
|
|
35150
|
-
|
|
35151
|
-
|
|
35243
|
+
if (parsed.kind === "main") {
|
|
35244
|
+
await applyMainQueueRehydrate(parsed.taskId, orchestrator, log);
|
|
35245
|
+
} else {
|
|
35246
|
+
await applyThreadQueueRehydrate(
|
|
35247
|
+
key,
|
|
35248
|
+
parsed.taskId,
|
|
35249
|
+
parsed.threadId,
|
|
35250
|
+
orchestrator,
|
|
35251
|
+
persistence,
|
|
35252
|
+
log
|
|
35253
|
+
);
|
|
35152
35254
|
}
|
|
35153
35255
|
}
|
|
35154
35256
|
}
|
|
@@ -69677,6 +69779,7 @@ async function runAbandonedSweep(deps) {
|
|
|
69677
69779
|
try {
|
|
69678
69780
|
await deps.setAbandonedAt(taskId, now);
|
|
69679
69781
|
markedAbandoned.push(taskId);
|
|
69782
|
+
deps.onTaskAbandoned(taskId, task, now);
|
|
69680
69783
|
} catch (err) {
|
|
69681
69784
|
deps.log?.({
|
|
69682
69785
|
event: "abandoned_sweep_mark_failed",
|
|
@@ -69716,6 +69819,222 @@ function createAbandonedSweeper(deps, intervalMs = ABANDONED_SWEEP_INTERVAL_MS)
|
|
|
69716
69819
|
};
|
|
69717
69820
|
}
|
|
69718
69821
|
|
|
69822
|
+
// src/services/roi/inject-shipyard-trailer.ts
|
|
69823
|
+
function buildDefaultTrailerInjectDeps() {
|
|
69824
|
+
return {
|
|
69825
|
+
readCommitMessage: async (cwd) => runWithTimeout("git", ["log", "-1", "--pretty=%B"], cwd, 5e3),
|
|
69826
|
+
amendCommit: async (cwd, message) => {
|
|
69827
|
+
await runWithTimeout("git", ["commit", "--amend", "--no-edit", "-m", message], cwd, 15e3);
|
|
69828
|
+
},
|
|
69829
|
+
getHeadSha: async (cwd) => (await runWithTimeout("git", ["rev-parse", "HEAD"], cwd, 5e3)).trim(),
|
|
69830
|
+
getCurrentBranch: async (cwd) => (await runWithTimeout("git", ["rev-parse", "--abbrev-ref", "HEAD"], cwd, 5e3)).trim(),
|
|
69831
|
+
getRepoSlug: async (cwd) => {
|
|
69832
|
+
const url = (await runWithTimeout("git", ["config", "--get", "remote.origin.url"], cwd, 5e3).catch(
|
|
69833
|
+
() => ""
|
|
69834
|
+
)).trim();
|
|
69835
|
+
const match2 = url.match(/[:/]([^/:]+\/[^/:]+?)(\.git)?\s*$/);
|
|
69836
|
+
return match2?.[1] ?? "";
|
|
69837
|
+
}
|
|
69838
|
+
};
|
|
69839
|
+
}
|
|
69840
|
+
async function injectShipyardTrailer(ctx, deps) {
|
|
69841
|
+
const original = await deps.readCommitMessage(ctx.cwd);
|
|
69842
|
+
if (hasShipyardTrailer(original)) {
|
|
69843
|
+
return { injected: false, reason: "already_trailered" };
|
|
69844
|
+
}
|
|
69845
|
+
const newMessage = appendTrailerToMessage(original, ctx.trailer);
|
|
69846
|
+
await deps.amendCommit(ctx.cwd, newMessage);
|
|
69847
|
+
const [commitSha, branch, repo] = await Promise.all([
|
|
69848
|
+
deps.getHeadSha(ctx.cwd),
|
|
69849
|
+
deps.getCurrentBranch(ctx.cwd),
|
|
69850
|
+
deps.getRepoSlug(ctx.cwd)
|
|
69851
|
+
]);
|
|
69852
|
+
return { injected: true, commitSha, branch, repo };
|
|
69853
|
+
}
|
|
69854
|
+
|
|
69855
|
+
// src/services/roi/commit-sweep.ts
|
|
69856
|
+
async function fetchCurrentHead(cwd) {
|
|
69857
|
+
return (await runWithTimeout("git", ["rev-parse", "HEAD"], cwd, 5e3)).trim();
|
|
69858
|
+
}
|
|
69859
|
+
async function fetchCommits(cwd, lastSeenSha) {
|
|
69860
|
+
let args;
|
|
69861
|
+
if (lastSeenSha) {
|
|
69862
|
+
const inHistory = await runWithTimeout("git", ["cat-file", "-t", lastSeenSha], cwd, 5e3).then(
|
|
69863
|
+
(t) => t.trim() === "commit",
|
|
69864
|
+
() => false
|
|
69865
|
+
);
|
|
69866
|
+
if (inHistory) {
|
|
69867
|
+
args = ["log", `${lastSeenSha}..HEAD`, "--pretty=%H%n%B%x00", "--reverse"];
|
|
69868
|
+
} else {
|
|
69869
|
+
args = ["log", "HEAD", "--since=5 minutes ago", "--pretty=%H%n%B%x00", "--reverse"];
|
|
69870
|
+
}
|
|
69871
|
+
} else {
|
|
69872
|
+
args = ["log", "HEAD", "--since=5 minutes ago", "--pretty=%H%n%B%x00", "--reverse"];
|
|
69873
|
+
}
|
|
69874
|
+
const raw = await runWithTimeout("git", args, cwd, 1e4);
|
|
69875
|
+
if (!raw) return [];
|
|
69876
|
+
return raw.split("\0").map((chunk) => chunk.trim()).filter(Boolean).map((chunk) => {
|
|
69877
|
+
const newlineIdx = chunk.indexOf("\n");
|
|
69878
|
+
if (newlineIdx === -1) return { sha: chunk.trim(), message: "" };
|
|
69879
|
+
const sha = chunk.slice(0, newlineIdx).trim();
|
|
69880
|
+
const message = chunk.slice(newlineIdx + 1).trim();
|
|
69881
|
+
return { sha, message };
|
|
69882
|
+
}).filter((c) => Boolean(c.sha));
|
|
69883
|
+
}
|
|
69884
|
+
async function handleHeadCommit(commit, input, deps) {
|
|
69885
|
+
const { cwd, taskId, userId } = input;
|
|
69886
|
+
const { sha } = commit;
|
|
69887
|
+
const payload = deps.getAttributionPayload(taskId);
|
|
69888
|
+
if (!payload) return;
|
|
69889
|
+
const trailer = {
|
|
69890
|
+
version: TRAILER_SCHEMA_VERSION,
|
|
69891
|
+
taskId,
|
|
69892
|
+
sessionId: payload.sessionId,
|
|
69893
|
+
model: payload.model,
|
|
69894
|
+
tokens: payload.tokens,
|
|
69895
|
+
costUsd: payload.costUsd,
|
|
69896
|
+
turnCount: payload.turnCount,
|
|
69897
|
+
attributionType: "originated",
|
|
69898
|
+
clientVersion: "shipyard-daemon"
|
|
69899
|
+
};
|
|
69900
|
+
try {
|
|
69901
|
+
const result = await deps.injectTrailer({ cwd, trailer }, buildDefaultTrailerInjectDeps());
|
|
69902
|
+
if (!result.injected) {
|
|
69903
|
+
if (result.reason !== "already_trailered") {
|
|
69904
|
+
deps.log({ event: "roi_trailer_not_injected", taskId, sha, reason: result.reason });
|
|
69905
|
+
}
|
|
69906
|
+
await deps.addAttributedCommitSha(taskId, sha);
|
|
69907
|
+
return;
|
|
69908
|
+
}
|
|
69909
|
+
const newSha = result.commitSha;
|
|
69910
|
+
if (newSha && result.repo && result.branch) {
|
|
69911
|
+
emitCommitAttributed(deps.metricsCollector, {
|
|
69912
|
+
taskId,
|
|
69913
|
+
userId,
|
|
69914
|
+
commitSha: newSha,
|
|
69915
|
+
repo: result.repo,
|
|
69916
|
+
branch: result.branch,
|
|
69917
|
+
model: trailer.model,
|
|
69918
|
+
tokens: trailer.tokens,
|
|
69919
|
+
costUsd: trailer.costUsd,
|
|
69920
|
+
turnCount: trailer.turnCount,
|
|
69921
|
+
attributionType: "originated"
|
|
69922
|
+
});
|
|
69923
|
+
await deps.addAttributedCommitSha(taskId, newSha);
|
|
69924
|
+
}
|
|
69925
|
+
} catch (err) {
|
|
69926
|
+
deps.log({
|
|
69927
|
+
event: "roi_trailer_injection_failed",
|
|
69928
|
+
taskId,
|
|
69929
|
+
sha,
|
|
69930
|
+
error: err instanceof Error ? err.message : String(err)
|
|
69931
|
+
});
|
|
69932
|
+
}
|
|
69933
|
+
}
|
|
69934
|
+
async function handleNonHeadCommit(commit, input, deps) {
|
|
69935
|
+
const { cwd, taskId, userId } = input;
|
|
69936
|
+
const { sha } = commit;
|
|
69937
|
+
const payload = deps.getAttributionPayload(taskId);
|
|
69938
|
+
if (!payload) {
|
|
69939
|
+
deps.log({ event: "roi_scan_skip_no_payload", taskId, sha });
|
|
69940
|
+
return;
|
|
69941
|
+
}
|
|
69942
|
+
const injectDeps = buildDefaultTrailerInjectDeps();
|
|
69943
|
+
try {
|
|
69944
|
+
const [branch, repo] = await Promise.all([
|
|
69945
|
+
injectDeps.getCurrentBranch(cwd),
|
|
69946
|
+
injectDeps.getRepoSlug(cwd)
|
|
69947
|
+
]);
|
|
69948
|
+
emitCommitAttributed(deps.metricsCollector, {
|
|
69949
|
+
taskId,
|
|
69950
|
+
userId,
|
|
69951
|
+
commitSha: sha,
|
|
69952
|
+
repo,
|
|
69953
|
+
branch,
|
|
69954
|
+
model: payload.model,
|
|
69955
|
+
tokens: payload.tokens,
|
|
69956
|
+
costUsd: payload.costUsd,
|
|
69957
|
+
turnCount: payload.turnCount,
|
|
69958
|
+
attributionType: "extended"
|
|
69959
|
+
});
|
|
69960
|
+
await deps.addAttributedCommitSha(taskId, sha);
|
|
69961
|
+
} catch (err) {
|
|
69962
|
+
deps.log({
|
|
69963
|
+
event: "roi_extended_attribution_failed",
|
|
69964
|
+
taskId,
|
|
69965
|
+
sha,
|
|
69966
|
+
error: err instanceof Error ? err.message : String(err)
|
|
69967
|
+
});
|
|
69968
|
+
}
|
|
69969
|
+
}
|
|
69970
|
+
async function scanAndAttributeCommits(input, deps) {
|
|
69971
|
+
const { cwd, taskId, lastSeenSha, attributedCommitShas } = input;
|
|
69972
|
+
const [commits, headSha] = await Promise.all([
|
|
69973
|
+
fetchCommits(cwd, lastSeenSha),
|
|
69974
|
+
fetchCurrentHead(cwd)
|
|
69975
|
+
]);
|
|
69976
|
+
if (commits.length === 0) {
|
|
69977
|
+
if (headSha !== lastSeenSha) {
|
|
69978
|
+
await deps.setLastCommitScanSha(taskId, headSha);
|
|
69979
|
+
}
|
|
69980
|
+
return;
|
|
69981
|
+
}
|
|
69982
|
+
for (const commit of commits) {
|
|
69983
|
+
const { sha, message } = commit;
|
|
69984
|
+
if (hasShipyardTrailer(message)) continue;
|
|
69985
|
+
if (attributedCommitShas.includes(sha)) continue;
|
|
69986
|
+
if (sha === headSha && deps.amendHead) {
|
|
69987
|
+
await handleHeadCommit(commit, input, deps);
|
|
69988
|
+
} else {
|
|
69989
|
+
await handleNonHeadCommit(commit, input, deps);
|
|
69990
|
+
}
|
|
69991
|
+
}
|
|
69992
|
+
await deps.setLastCommitScanSha(taskId, headSha);
|
|
69993
|
+
}
|
|
69994
|
+
var CommitSweepService = class {
|
|
69995
|
+
#deps;
|
|
69996
|
+
#intervalMs;
|
|
69997
|
+
#timer = null;
|
|
69998
|
+
constructor(deps, intervalMs = 6e4) {
|
|
69999
|
+
this.#deps = deps;
|
|
70000
|
+
this.#intervalMs = intervalMs;
|
|
70001
|
+
}
|
|
70002
|
+
start() {
|
|
70003
|
+
if (this.#timer) return;
|
|
70004
|
+
this.#timer = setInterval(() => {
|
|
70005
|
+
this.#runSweep().catch((err) => {
|
|
70006
|
+
this.#deps.log({
|
|
70007
|
+
event: "commit_sweep_failed",
|
|
70008
|
+
error: err instanceof Error ? err.message : String(err)
|
|
70009
|
+
});
|
|
70010
|
+
});
|
|
70011
|
+
}, this.#intervalMs);
|
|
70012
|
+
this.#timer.unref?.();
|
|
70013
|
+
}
|
|
70014
|
+
stop() {
|
|
70015
|
+
if (this.#timer) {
|
|
70016
|
+
clearInterval(this.#timer);
|
|
70017
|
+
this.#timer = null;
|
|
70018
|
+
}
|
|
70019
|
+
}
|
|
70020
|
+
async #runSweep() {
|
|
70021
|
+
const tasks = this.#deps.getActiveTasks();
|
|
70022
|
+
for (const { taskId, cwd, userId } of tasks) {
|
|
70023
|
+
const attribution = await this.#deps.getTaskAttribution(taskId);
|
|
70024
|
+
await scanAndAttributeCommits(
|
|
70025
|
+
{
|
|
70026
|
+
cwd,
|
|
70027
|
+
taskId,
|
|
70028
|
+
userId: attribution.userId || userId,
|
|
70029
|
+
lastSeenSha: attribution.lastSeenSha,
|
|
70030
|
+
attributedCommitShas: attribution.attributedCommitShas
|
|
70031
|
+
},
|
|
70032
|
+
this.#deps
|
|
70033
|
+
);
|
|
70034
|
+
}
|
|
70035
|
+
}
|
|
70036
|
+
};
|
|
70037
|
+
|
|
69719
70038
|
// ../../node_modules/.pnpm/croner@10.0.1/node_modules/croner/dist/croner.js
|
|
69720
70039
|
function T(s2) {
|
|
69721
70040
|
return Date.UTC(s2.y, s2.m - 1, s2.d, s2.h, s2.i, s2.s);
|
|
@@ -81033,6 +81352,50 @@ function buildJsonlConversationStore(dataDir) {
|
|
|
81033
81352
|
}
|
|
81034
81353
|
return messages;
|
|
81035
81354
|
}
|
|
81355
|
+
function contentFingerprint(message) {
|
|
81356
|
+
const texts = message.content.filter((b2) => b2.type === "text").map((b2) => b2.type === "text" ? b2.text : "").join("\0");
|
|
81357
|
+
return `${message.participantId}${texts}`;
|
|
81358
|
+
}
|
|
81359
|
+
function lookupByCorrelationId(existing, correlationId) {
|
|
81360
|
+
return existing.find((m2) => m2.senderKind === "human" && m2.correlationId === correlationId);
|
|
81361
|
+
}
|
|
81362
|
+
function lookupBySdkUuid(existing, sdkUuid) {
|
|
81363
|
+
return existing.find((m2) => m2.senderKind === "human" && m2.sdkUuid === sdkUuid);
|
|
81364
|
+
}
|
|
81365
|
+
function lookupByFingerprint(existing, fingerprint, windowStart) {
|
|
81366
|
+
return existing.find(
|
|
81367
|
+
(m2) => m2.senderKind === "human" && !m2.correlationId && !m2.sdkUuid && m2.timestamp >= windowStart && contentFingerprint(m2) === fingerprint
|
|
81368
|
+
);
|
|
81369
|
+
}
|
|
81370
|
+
function decideDedupByCorrelation(existing, message) {
|
|
81371
|
+
if (!message.correlationId) return null;
|
|
81372
|
+
const found2 = lookupByCorrelationId(existing, message.correlationId);
|
|
81373
|
+
if (!found2) return null;
|
|
81374
|
+
return { kind: "duplicate", seqNo: found2.seqNo, dedupKey: `corr:${message.correlationId}` };
|
|
81375
|
+
}
|
|
81376
|
+
function decideDedupBySdkUuid(existing, message) {
|
|
81377
|
+
if (!message.sdkUuid) return null;
|
|
81378
|
+
const found2 = lookupBySdkUuid(existing, message.sdkUuid);
|
|
81379
|
+
if (!found2) return null;
|
|
81380
|
+
return { kind: "duplicate", seqNo: found2.seqNo, dedupKey: `sdk:${message.sdkUuid}` };
|
|
81381
|
+
}
|
|
81382
|
+
function decideDedupByFingerprint(existing, message, dedupWindowMs, now) {
|
|
81383
|
+
if (message.correlationId || message.sdkUuid) return null;
|
|
81384
|
+
const fingerprint = contentFingerprint(message);
|
|
81385
|
+
const found2 = lookupByFingerprint(existing, fingerprint, now - dedupWindowMs);
|
|
81386
|
+
if (!found2) return null;
|
|
81387
|
+
return { kind: "duplicate", seqNo: found2.seqNo, dedupKey: `fp:${fingerprint}` };
|
|
81388
|
+
}
|
|
81389
|
+
function classifyDedupResult(existing, message, currentSeq, dedupWindowMs, now) {
|
|
81390
|
+
const byCorr = decideDedupByCorrelation(existing, message);
|
|
81391
|
+
if (byCorr) return byCorr;
|
|
81392
|
+
const bySdk = decideDedupBySdkUuid(existing, message);
|
|
81393
|
+
if (bySdk) return bySdk;
|
|
81394
|
+
const byFp = decideDedupByFingerprint(existing, message, dedupWindowMs, now);
|
|
81395
|
+
if (byFp) return byFp;
|
|
81396
|
+
const seqNo = currentSeq !== void 0 ? currentSeq + 1 : existing.length;
|
|
81397
|
+
return { kind: "append", seqNo };
|
|
81398
|
+
}
|
|
81036
81399
|
return {
|
|
81037
81400
|
async appendMessage(message) {
|
|
81038
81401
|
let resolveSeqNo;
|
|
@@ -81064,6 +81427,88 @@ function buildJsonlConversationStore(dataDir) {
|
|
|
81064
81427
|
channelQueues.set(message.channelId, next);
|
|
81065
81428
|
return seqNoPromise;
|
|
81066
81429
|
},
|
|
81430
|
+
async appendMessageDeduped(message, opts = {}) {
|
|
81431
|
+
const DEDUP_WINDOW_MS = opts.dedupWindowMs ?? 6e4;
|
|
81432
|
+
let resolveResult;
|
|
81433
|
+
let rejectResult;
|
|
81434
|
+
const resultPromise = new Promise((resolve4, reject) => {
|
|
81435
|
+
resolveResult = resolve4;
|
|
81436
|
+
rejectResult = reject;
|
|
81437
|
+
});
|
|
81438
|
+
const prev = channelQueues.get(message.channelId) ?? Promise.resolve();
|
|
81439
|
+
const next = prev.then(async () => {
|
|
81440
|
+
await ensureDir();
|
|
81441
|
+
const existing = await readLines(message.channelId);
|
|
81442
|
+
const currentSeq = seqCounters.get(message.channelId);
|
|
81443
|
+
const decision = classifyDedupResult(
|
|
81444
|
+
existing,
|
|
81445
|
+
message,
|
|
81446
|
+
currentSeq,
|
|
81447
|
+
DEDUP_WINDOW_MS,
|
|
81448
|
+
Date.now()
|
|
81449
|
+
);
|
|
81450
|
+
if (decision.kind === "duplicate") {
|
|
81451
|
+
resolveResult({
|
|
81452
|
+
seqNo: decision.seqNo,
|
|
81453
|
+
isDuplicate: true,
|
|
81454
|
+
dedupKey: decision.dedupKey
|
|
81455
|
+
});
|
|
81456
|
+
return;
|
|
81457
|
+
}
|
|
81458
|
+
seqCounters.set(message.channelId, decision.seqNo);
|
|
81459
|
+
const { channelId: _channelId, ...rest } = message;
|
|
81460
|
+
const line = JSON.stringify(rest);
|
|
81461
|
+
await appendFile2(channelPath(message.channelId), `${line}
|
|
81462
|
+
`, "utf-8");
|
|
81463
|
+
resolveResult({ seqNo: decision.seqNo, isDuplicate: false, dedupKey: null });
|
|
81464
|
+
}).catch((err) => {
|
|
81465
|
+
rejectResult(err);
|
|
81466
|
+
});
|
|
81467
|
+
channelQueues.set(message.channelId, next);
|
|
81468
|
+
return resultPromise;
|
|
81469
|
+
},
|
|
81470
|
+
async stampSdkUuid(channelId, primaryCorrelationId, sdkUuid, batchCorrelationIds) {
|
|
81471
|
+
let resolveVoid;
|
|
81472
|
+
let rejectVoid;
|
|
81473
|
+
const voidPromise = new Promise((resolve4, reject) => {
|
|
81474
|
+
resolveVoid = resolve4;
|
|
81475
|
+
rejectVoid = reject;
|
|
81476
|
+
});
|
|
81477
|
+
const prev = channelQueues.get(channelId) ?? Promise.resolve();
|
|
81478
|
+
const next = prev.then(async () => {
|
|
81479
|
+
const messages = await readLines(channelId);
|
|
81480
|
+
const target = messages.find(
|
|
81481
|
+
(m2) => m2.senderKind === "human" && m2.correlationId === primaryCorrelationId && !m2.sdkUuid
|
|
81482
|
+
);
|
|
81483
|
+
if (!target) {
|
|
81484
|
+
resolveVoid();
|
|
81485
|
+
return;
|
|
81486
|
+
}
|
|
81487
|
+
const secondaryIds = batchCorrelationIds && batchCorrelationIds.length > 0 ? batchCorrelationIds : void 0;
|
|
81488
|
+
const lines = messages.map((m2) => {
|
|
81489
|
+
const { seqNo: _seqNo, channelId: _channelId, ...rest } = m2;
|
|
81490
|
+
if (m2.seqNo === target.seqNo) {
|
|
81491
|
+
return JSON.stringify({
|
|
81492
|
+
...rest,
|
|
81493
|
+
sdkUuid,
|
|
81494
|
+
...secondaryIds ? { batchCorrelationIds: secondaryIds } : {}
|
|
81495
|
+
});
|
|
81496
|
+
}
|
|
81497
|
+
return JSON.stringify(rest);
|
|
81498
|
+
});
|
|
81499
|
+
const content = lines.length > 0 ? `${lines.join("\n")}
|
|
81500
|
+
` : "";
|
|
81501
|
+
const filePath = channelPath(channelId);
|
|
81502
|
+
const tmpPath = `${filePath}.tmp`;
|
|
81503
|
+
await writeFile16(tmpPath, content, "utf-8");
|
|
81504
|
+
await rename10(tmpPath, filePath);
|
|
81505
|
+
resolveVoid();
|
|
81506
|
+
}).catch((err) => {
|
|
81507
|
+
rejectVoid(err);
|
|
81508
|
+
});
|
|
81509
|
+
channelQueues.set(channelId, next);
|
|
81510
|
+
return voidPromise;
|
|
81511
|
+
},
|
|
81067
81512
|
async getMessages(channelId) {
|
|
81068
81513
|
return readLines(channelId);
|
|
81069
81514
|
},
|
|
@@ -81158,6 +81603,17 @@ function buildObservableConversationStore(inner) {
|
|
|
81158
81603
|
notify(message.channelId, fullMessage);
|
|
81159
81604
|
return seqNo;
|
|
81160
81605
|
},
|
|
81606
|
+
async appendMessageDeduped(message, opts) {
|
|
81607
|
+
const result = await inner.appendMessageDeduped(message, opts);
|
|
81608
|
+
if (!result.isDuplicate) {
|
|
81609
|
+
const fullMessage = { ...message, seqNo: result.seqNo };
|
|
81610
|
+
notify(message.channelId, fullMessage);
|
|
81611
|
+
}
|
|
81612
|
+
return result;
|
|
81613
|
+
},
|
|
81614
|
+
stampSdkUuid(channelId, correlationId, sdkUuid, batchCorrelationIds) {
|
|
81615
|
+
return inner.stampSdkUuid(channelId, correlationId, sdkUuid, batchCorrelationIds);
|
|
81616
|
+
},
|
|
81161
81617
|
getMessages(channelId) {
|
|
81162
81618
|
return inner.getMessages(channelId);
|
|
81163
81619
|
},
|
|
@@ -81350,6 +81806,7 @@ function buildJsonDocumentStore(opts) {
|
|
|
81350
81806
|
if (!existing) return;
|
|
81351
81807
|
const parsed = recordSchema.parse(existing);
|
|
81352
81808
|
const updated = fn(parsed);
|
|
81809
|
+
if (updated === parsed) return;
|
|
81353
81810
|
store.records[id] = updated;
|
|
81354
81811
|
await atomicWrite3(store);
|
|
81355
81812
|
notify({ kind: "set", id, data: updated });
|
|
@@ -81612,7 +82069,7 @@ var EVENT_BATCHING = {
|
|
|
81612
82069
|
/** Resources: sliding window for file/plan/git/task pushes. */
|
|
81613
82070
|
resources: { idleMs: 1e4, maxWindowMs: 6e4 },
|
|
81614
82071
|
/** Messages: sliding window for collab message micro-batching. */
|
|
81615
|
-
messages: { idleMs:
|
|
82072
|
+
messages: { idleMs: 250, maxWindowMs: 3e4 }
|
|
81616
82073
|
};
|
|
81617
82074
|
var MAX_COLLAB_BATCH_SIZE = 4;
|
|
81618
82075
|
var SlidingWindowTimer = class {
|
|
@@ -81702,18 +82159,58 @@ function formatCollabBatch(batch) {
|
|
|
81702
82159
|
for (const block2 of nonTextBlocks) result.push(block2);
|
|
81703
82160
|
return result;
|
|
81704
82161
|
}
|
|
82162
|
+
var IDLE_STATES = /* @__PURE__ */ new Set(["warm_idle", "resumable_idle", "cold_idle"]);
|
|
81705
82163
|
var CollabMessageQueue = class {
|
|
81706
82164
|
#deps;
|
|
81707
82165
|
#slots = /* @__PURE__ */ new Map();
|
|
81708
82166
|
#maxBatchSize;
|
|
81709
82167
|
#disposed = false;
|
|
82168
|
+
#unsubscribeThreadState = null;
|
|
81710
82169
|
constructor(deps) {
|
|
81711
82170
|
this.#deps = deps;
|
|
81712
82171
|
this.#maxBatchSize = deps.maxBatchSize ?? MAX_COLLAB_BATCH_SIZE;
|
|
82172
|
+
if (deps.subscribeToThreadState) {
|
|
82173
|
+
this.#unsubscribeThreadState = deps.subscribeToThreadState((newState) => {
|
|
82174
|
+
if (IDLE_STATES.has(newState)) {
|
|
82175
|
+
this.onTurnComplete();
|
|
82176
|
+
}
|
|
82177
|
+
});
|
|
82178
|
+
}
|
|
82179
|
+
}
|
|
82180
|
+
/**
|
|
82181
|
+
* Wire Thread state-change notifications into this queue after construction.
|
|
82182
|
+
*
|
|
82183
|
+
* Use this when the Thread isn't available yet at queue-construction time
|
|
82184
|
+
* (e.g., the main collab queue, which is created before `#mainThread`).
|
|
82185
|
+
* Must be called at most once — subsequent calls are no-ops.
|
|
82186
|
+
*
|
|
82187
|
+
* On Thread transition to any idle state (`warm_idle`, `resumable_idle`,
|
|
82188
|
+
* `cold_idle`), releases all slots blocked in `waiting_for_turn`.
|
|
82189
|
+
*/
|
|
82190
|
+
wireThreadStateSubscription(subscribe2) {
|
|
82191
|
+
if (this.#unsubscribeThreadState) return;
|
|
82192
|
+
this.#unsubscribeThreadState = subscribe2((newState) => {
|
|
82193
|
+
if (IDLE_STATES.has(newState)) {
|
|
82194
|
+
this.onTurnComplete();
|
|
82195
|
+
}
|
|
82196
|
+
});
|
|
81713
82197
|
}
|
|
81714
82198
|
enqueue(entry) {
|
|
81715
82199
|
this.#enqueueInternal(entry, { persist: true });
|
|
81716
82200
|
}
|
|
82201
|
+
#transitionSlot(key, slot, to, trigger) {
|
|
82202
|
+
const from2 = slot.state;
|
|
82203
|
+
if (from2 === to) return;
|
|
82204
|
+
slot.state = to;
|
|
82205
|
+
this.#deps.log({
|
|
82206
|
+
event: "collab_queue_slot_transition",
|
|
82207
|
+
participantId: key,
|
|
82208
|
+
queueKey: this.#deps.queueKey ?? null,
|
|
82209
|
+
from: from2,
|
|
82210
|
+
to,
|
|
82211
|
+
trigger
|
|
82212
|
+
});
|
|
82213
|
+
}
|
|
81717
82214
|
#enqueueInternal(entry, opts) {
|
|
81718
82215
|
if (this.#disposed) return;
|
|
81719
82216
|
const participants = opts.participantsOverride ?? this.#deps.getCollabParticipants();
|
|
@@ -81733,7 +82230,7 @@ var CollabMessageQueue = class {
|
|
|
81733
82230
|
});
|
|
81734
82231
|
return;
|
|
81735
82232
|
}
|
|
81736
|
-
slot
|
|
82233
|
+
this.#transitionSlot(key, slot, "accumulating", "enqueue");
|
|
81737
82234
|
if (slot.pending.length >= this.#maxBatchSize) {
|
|
81738
82235
|
this.#flushSlot(key);
|
|
81739
82236
|
return;
|
|
@@ -81744,15 +82241,23 @@ var CollabMessageQueue = class {
|
|
|
81744
82241
|
* Signal that the agent turn finished. Every participant slot that
|
|
81745
82242
|
* was blocked in waiting_for_turn is released. Slots with pending
|
|
81746
82243
|
* messages transition back to accumulating with a fresh idle window.
|
|
82244
|
+
*
|
|
82245
|
+
* Defers internally via queueMicrotask (Invariant #11) so callers can
|
|
82246
|
+
* invoke this synchronously from library callbacks without risking re-entry.
|
|
81747
82247
|
*/
|
|
81748
82248
|
onTurnComplete() {
|
|
82249
|
+
queueMicrotask(() => {
|
|
82250
|
+
this.#doTurnComplete();
|
|
82251
|
+
});
|
|
82252
|
+
}
|
|
82253
|
+
#doTurnComplete() {
|
|
81749
82254
|
for (const [key, slot] of this.#slots) {
|
|
81750
82255
|
if (slot.state !== "waiting_for_turn") continue;
|
|
81751
82256
|
if (slot.pending.length > 0) {
|
|
81752
|
-
slot
|
|
82257
|
+
this.#transitionSlot(key, slot, "accumulating", "turn_complete_with_pending");
|
|
81753
82258
|
slot.timer.schedule();
|
|
81754
82259
|
} else {
|
|
81755
|
-
slot
|
|
82260
|
+
this.#transitionSlot(key, slot, "idle", "turn_complete_drained");
|
|
81756
82261
|
this.#slots.delete(key);
|
|
81757
82262
|
}
|
|
81758
82263
|
}
|
|
@@ -81764,7 +82269,7 @@ var CollabMessageQueue = class {
|
|
|
81764
82269
|
slot.pending.length = 0;
|
|
81765
82270
|
slot.timer.cancel();
|
|
81766
82271
|
slot.timer.reset();
|
|
81767
|
-
slot
|
|
82272
|
+
this.#transitionSlot(key, slot, "idle", "cancel_queued");
|
|
81768
82273
|
this.#clearPersistedFor(key);
|
|
81769
82274
|
}
|
|
81770
82275
|
this.#slots.clear();
|
|
@@ -81775,11 +82280,13 @@ var CollabMessageQueue = class {
|
|
|
81775
82280
|
slot.pending.length = 0;
|
|
81776
82281
|
slot.timer.dispose();
|
|
81777
82282
|
this.#clearPersistedFor(key);
|
|
81778
|
-
slot
|
|
82283
|
+
this.#transitionSlot(key, slot, "idle", "reset");
|
|
81779
82284
|
}
|
|
81780
82285
|
this.#slots.clear();
|
|
81781
82286
|
}
|
|
81782
82287
|
dispose() {
|
|
82288
|
+
this.#unsubscribeThreadState?.();
|
|
82289
|
+
this.#unsubscribeThreadState = null;
|
|
81783
82290
|
this.#flushAllImmediate();
|
|
81784
82291
|
for (const slot of this.#slots.values()) {
|
|
81785
82292
|
slot.timer.dispose();
|
|
@@ -81861,17 +82368,17 @@ var CollabMessageQueue = class {
|
|
|
81861
82368
|
slot.timer.cancel();
|
|
81862
82369
|
slot.timer.reset();
|
|
81863
82370
|
if (slot.pending.length === 0) {
|
|
81864
|
-
slot
|
|
82371
|
+
this.#transitionSlot(key, slot, "idle", "flush_empty");
|
|
81865
82372
|
return;
|
|
81866
82373
|
}
|
|
81867
82374
|
const batch = slot.pending.splice(0);
|
|
81868
82375
|
const content = formatCollabBatch(batch);
|
|
81869
|
-
slot
|
|
82376
|
+
this.#transitionSlot(key, slot, "waiting_for_turn", "flush");
|
|
81870
82377
|
try {
|
|
81871
82378
|
this.#deps.forwardBatch(content, batch);
|
|
81872
82379
|
} catch (err) {
|
|
81873
82380
|
slot.pending.unshift(...batch);
|
|
81874
|
-
slot
|
|
82381
|
+
this.#transitionSlot(key, slot, "accumulating", "forward_failed");
|
|
81875
82382
|
slot.timer.schedule();
|
|
81876
82383
|
this.#deps.log({
|
|
81877
82384
|
event: "collab_queue_forward_failed",
|
|
@@ -81881,6 +82388,12 @@ var CollabMessageQueue = class {
|
|
|
81881
82388
|
return;
|
|
81882
82389
|
}
|
|
81883
82390
|
this.#clearPersistedFor(key);
|
|
82391
|
+
if (this.#deps.onBatchForwarded) {
|
|
82392
|
+
const batchCorrelationIds = batch.map((e) => e.correlationId).filter((id) => id !== void 0);
|
|
82393
|
+
if (batchCorrelationIds.length > 0) {
|
|
82394
|
+
this.#deps.onBatchForwarded(batchCorrelationIds);
|
|
82395
|
+
}
|
|
82396
|
+
}
|
|
81884
82397
|
this.#deps.log({
|
|
81885
82398
|
event: "collab_queue_flushed",
|
|
81886
82399
|
participantId: key,
|
|
@@ -81901,6 +82414,12 @@ var CollabMessageQueue = class {
|
|
|
81901
82414
|
const content = formatCollabBatch(batch);
|
|
81902
82415
|
this.#deps.forwardBatch(content, batch);
|
|
81903
82416
|
this.#clearPersistedFor(key);
|
|
82417
|
+
if (this.#deps.onBatchForwarded) {
|
|
82418
|
+
const batchCorrelationIds = batch.map((e) => e.correlationId).filter((id) => id !== void 0);
|
|
82419
|
+
if (batchCorrelationIds.length > 0) {
|
|
82420
|
+
this.#deps.onBatchForwarded(batchCorrelationIds);
|
|
82421
|
+
}
|
|
82422
|
+
}
|
|
81904
82423
|
this.#deps.log({
|
|
81905
82424
|
event: "collab_queue_flushed_immediate",
|
|
81906
82425
|
participantId: key,
|
|
@@ -82634,6 +83153,7 @@ function handleSpawning(snapshot, event) {
|
|
|
82634
83153
|
b2.log("spawning", "cold_idle", event.type);
|
|
82635
83154
|
b2.taskStatus("input_required");
|
|
82636
83155
|
b2.emitError(event.error, "spawn_failed");
|
|
83156
|
+
b2.effects.push({ type: "re_queue_initial", content: event.lastInitialContent ?? [] });
|
|
82637
83157
|
return { state: "cold_idle", sessionId: null, rewindAtMessageId: null, effects: b2.effects };
|
|
82638
83158
|
}
|
|
82639
83159
|
if (event.type === "sdk_error" || event.type === "subprocess_died") {
|
|
@@ -82729,14 +83249,19 @@ function exitStoppingAlive(snapshot, trigger) {
|
|
|
82729
83249
|
const { sessionId, rewindAtMessageId } = snapshot;
|
|
82730
83250
|
const b2 = createEffectBuilder();
|
|
82731
83251
|
b2.effects.push({ type: "clear_pending_inputs" });
|
|
82732
|
-
b2.effects.push({ type: "
|
|
83252
|
+
b2.effects.push({ type: "clear_unmarked_queue" });
|
|
82733
83253
|
b2.log("stopping", "warm_idle", trigger);
|
|
82734
83254
|
b2.taskStatus("input_required");
|
|
83255
|
+
b2.effects.push({ type: "push_message" });
|
|
82735
83256
|
return { state: "warm_idle", sessionId, rewindAtMessageId, effects: b2.effects };
|
|
82736
83257
|
}
|
|
82737
83258
|
function handleStopping(snapshot, event) {
|
|
82738
83259
|
const { sessionId, rewindAtMessageId } = snapshot;
|
|
82739
83260
|
const b2 = createEffectBuilder();
|
|
83261
|
+
if (event.type === "user_message") {
|
|
83262
|
+
b2.effects.push({ type: "mark_queue_preserve" });
|
|
83263
|
+
return { state: "stopping", sessionId, rewindAtMessageId, effects: b2.effects };
|
|
83264
|
+
}
|
|
82740
83265
|
if (event.type === "turn_complete") {
|
|
82741
83266
|
return exitStoppingAlive(snapshot, event.type);
|
|
82742
83267
|
}
|
|
@@ -82746,14 +83271,14 @@ function handleStopping(snapshot, event) {
|
|
|
82746
83271
|
if (event.type === "close_acknowledged") {
|
|
82747
83272
|
b2.log("stopping", "resumable_idle", event.type);
|
|
82748
83273
|
b2.effects.push({ type: "clear_pending_inputs" });
|
|
82749
|
-
b2.effects.push({ type: "
|
|
83274
|
+
b2.effects.push({ type: "clear_unmarked_queue" });
|
|
82750
83275
|
b2.taskStatus("input_required");
|
|
82751
83276
|
return { state: "resumable_idle", sessionId, rewindAtMessageId, effects: b2.effects };
|
|
82752
83277
|
}
|
|
82753
83278
|
if (event.type === "subprocess_died" || event.type === "sdk_error") {
|
|
82754
83279
|
b2.log("stopping", "resumable_idle", event.type);
|
|
82755
83280
|
b2.effects.push({ type: "clear_pending_inputs" });
|
|
82756
|
-
b2.effects.push({ type: "
|
|
83281
|
+
b2.effects.push({ type: "clear_unmarked_queue" });
|
|
82757
83282
|
b2.taskStatus("input_required");
|
|
82758
83283
|
return { state: "resumable_idle", sessionId, rewindAtMessageId, effects: b2.effects };
|
|
82759
83284
|
}
|
|
@@ -82761,7 +83286,7 @@ function handleStopping(snapshot, event) {
|
|
|
82761
83286
|
b2.log("stopping", "resumable_idle", event.type);
|
|
82762
83287
|
b2.effects.push({ type: "force_kill" });
|
|
82763
83288
|
b2.effects.push({ type: "clear_pending_inputs" });
|
|
82764
|
-
b2.effects.push({ type: "
|
|
83289
|
+
b2.effects.push({ type: "clear_unmarked_queue" });
|
|
82765
83290
|
b2.taskStatus("input_required");
|
|
82766
83291
|
return { state: "resumable_idle", sessionId, rewindAtMessageId, effects: b2.effects };
|
|
82767
83292
|
}
|
|
@@ -82823,6 +83348,12 @@ var AgentSessionManager = class {
|
|
|
82823
83348
|
}
|
|
82824
83349
|
sendMessage(content) {
|
|
82825
83350
|
this.#queue.push({ content });
|
|
83351
|
+
this.#config.log({
|
|
83352
|
+
event: "session_send_message",
|
|
83353
|
+
taskId: this.#config.taskId,
|
|
83354
|
+
queueDepth: this.#queue.length,
|
|
83355
|
+
eventType: "user_message"
|
|
83356
|
+
});
|
|
82826
83357
|
this.#dispatch({ type: "user_message" });
|
|
82827
83358
|
}
|
|
82828
83359
|
notifyInitReceived(sessionId) {
|
|
@@ -82852,8 +83383,8 @@ var AgentSessionManager = class {
|
|
|
82852
83383
|
notifyCloseAcknowledged() {
|
|
82853
83384
|
this.#dispatch({ type: "close_acknowledged" });
|
|
82854
83385
|
}
|
|
82855
|
-
notifySpawnFailed(error2) {
|
|
82856
|
-
this.#dispatch({ type: "spawn_failed", error: error2 });
|
|
83386
|
+
notifySpawnFailed(error2, lastInitialContent) {
|
|
83387
|
+
this.#dispatch({ type: "spawn_failed", error: error2, lastInitialContent });
|
|
82857
83388
|
}
|
|
82858
83389
|
addPendingInput(toolUseId, request) {
|
|
82859
83390
|
this.#pendingInputs.set(toolUseId, request);
|
|
@@ -82899,10 +83430,14 @@ var AgentSessionManager = class {
|
|
|
82899
83430
|
}
|
|
82900
83431
|
}
|
|
82901
83432
|
#processEvent(event) {
|
|
83433
|
+
const prevState = this.#state;
|
|
82902
83434
|
const result = transition(this.#snapshot(), event);
|
|
82903
83435
|
this.#state = result.state;
|
|
82904
83436
|
this.#sessionId = result.sessionId;
|
|
82905
83437
|
this.#rewindAtMessageId = result.rewindAtMessageId;
|
|
83438
|
+
if (this.#state !== prevState) {
|
|
83439
|
+
this.#config.onSessionStateChange(this.#state, prevState);
|
|
83440
|
+
}
|
|
82906
83441
|
this.#applyEffects(result.effects);
|
|
82907
83442
|
this.#manageTimers();
|
|
82908
83443
|
}
|
|
@@ -82914,17 +83449,9 @@ var AgentSessionManager = class {
|
|
|
82914
83449
|
this.#config.onSpawn(effect.reason, initial?.content ?? []);
|
|
82915
83450
|
break;
|
|
82916
83451
|
}
|
|
82917
|
-
case "push_message":
|
|
82918
|
-
|
|
82919
|
-
while (this.#queue.length > 0) {
|
|
82920
|
-
const msg = this.#queue.shift();
|
|
82921
|
-
if (msg) messages.push(msg.content);
|
|
82922
|
-
}
|
|
82923
|
-
if (messages.length > 0) {
|
|
82924
|
-
this.#config.onFlushQueue(messages);
|
|
82925
|
-
}
|
|
83452
|
+
case "push_message":
|
|
83453
|
+
this.#flushQueue();
|
|
82926
83454
|
break;
|
|
82927
|
-
}
|
|
82928
83455
|
case "interrupt":
|
|
82929
83456
|
this.#config.onInterrupt();
|
|
82930
83457
|
break;
|
|
@@ -82955,6 +83482,17 @@ var AgentSessionManager = class {
|
|
|
82955
83482
|
case "clear_queue":
|
|
82956
83483
|
this.#queue = [];
|
|
82957
83484
|
break;
|
|
83485
|
+
case "mark_queue_preserve":
|
|
83486
|
+
this.#markLastPreserved();
|
|
83487
|
+
break;
|
|
83488
|
+
case "clear_unmarked_queue":
|
|
83489
|
+
this.#queue = this.#queue.filter((q) => q.preserved);
|
|
83490
|
+
break;
|
|
83491
|
+
case "re_queue_initial":
|
|
83492
|
+
if (effect.content.length > 0) {
|
|
83493
|
+
this.#queue.unshift({ content: effect.content, preserved: true });
|
|
83494
|
+
}
|
|
83495
|
+
break;
|
|
82958
83496
|
default: {
|
|
82959
83497
|
const _exhaustive = effect;
|
|
82960
83498
|
throw new Error(`Unhandled effect: ${JSON.stringify(_exhaustive)}`);
|
|
@@ -82962,6 +83500,22 @@ var AgentSessionManager = class {
|
|
|
82962
83500
|
}
|
|
82963
83501
|
}
|
|
82964
83502
|
}
|
|
83503
|
+
#flushQueue() {
|
|
83504
|
+
const messages = [];
|
|
83505
|
+
while (this.#queue.length > 0) {
|
|
83506
|
+
const msg = this.#queue.shift();
|
|
83507
|
+
if (msg) messages.push(msg.content);
|
|
83508
|
+
}
|
|
83509
|
+
if (messages.length > 0) {
|
|
83510
|
+
this.#config.onFlushQueue(messages);
|
|
83511
|
+
}
|
|
83512
|
+
}
|
|
83513
|
+
#markLastPreserved() {
|
|
83514
|
+
if (this.#queue.length > 0) {
|
|
83515
|
+
const last = this.#queue[this.#queue.length - 1];
|
|
83516
|
+
if (last) last.preserved = true;
|
|
83517
|
+
}
|
|
83518
|
+
}
|
|
82965
83519
|
#manageTimers() {
|
|
82966
83520
|
if (this.#state === "warm_idle") {
|
|
82967
83521
|
this.#clearStopTimer();
|
|
@@ -83411,6 +83965,7 @@ var Thread = class {
|
|
|
83411
83965
|
#subprocess = null;
|
|
83412
83966
|
#asyncQueue = Promise.resolve();
|
|
83413
83967
|
#disposed = false;
|
|
83968
|
+
#stateChangeListeners = /* @__PURE__ */ new Set();
|
|
83414
83969
|
#permissionQueue = [];
|
|
83415
83970
|
#sendControlMessage;
|
|
83416
83971
|
#streamDeltaSinks = /* @__PURE__ */ new Set();
|
|
@@ -83496,7 +84051,8 @@ var Thread = class {
|
|
|
83496
84051
|
onInterrupt: () => this.#handleInterrupt(),
|
|
83497
84052
|
onClose: () => this.#handleClose(),
|
|
83498
84053
|
onForceKill: () => this.#handleForceKill(),
|
|
83499
|
-
log: config2.log
|
|
84054
|
+
log: config2.log,
|
|
84055
|
+
onSessionStateChange: (newState, prevState) => this.#notifyStateChange(newState, prevState)
|
|
83500
84056
|
},
|
|
83501
84057
|
initialSnapshot
|
|
83502
84058
|
);
|
|
@@ -83507,6 +84063,21 @@ var Thread = class {
|
|
|
83507
84063
|
get state() {
|
|
83508
84064
|
return this.#manager.state;
|
|
83509
84065
|
}
|
|
84066
|
+
/**
|
|
84067
|
+
* Subscribe to session state changes. The listener is called with the new
|
|
84068
|
+
* and previous SessionState whenever a transition occurs.
|
|
84069
|
+
*
|
|
84070
|
+
* Dispatch is microtask-deferred (Invariant #11) to prevent re-entry into
|
|
84071
|
+
* wasm/loro and other synchronous event emitters.
|
|
84072
|
+
*
|
|
84073
|
+
* Returns an unsubscribe function.
|
|
84074
|
+
*/
|
|
84075
|
+
onStateChange(listener) {
|
|
84076
|
+
this.#stateChangeListeners.add(listener);
|
|
84077
|
+
return () => {
|
|
84078
|
+
this.#stateChangeListeners.delete(listener);
|
|
84079
|
+
};
|
|
84080
|
+
}
|
|
83510
84081
|
get sessionId() {
|
|
83511
84082
|
return this.#ownSessionId ?? this.#manager.sessionId;
|
|
83512
84083
|
}
|
|
@@ -83618,6 +84189,7 @@ var Thread = class {
|
|
|
83618
84189
|
this.#permissionHandler.denyAllPending("Thread disposed");
|
|
83619
84190
|
this.#permissionQueue.length = 0;
|
|
83620
84191
|
this.#streamDeltaSinks.clear();
|
|
84192
|
+
this.#stateChangeListeners.clear();
|
|
83621
84193
|
this.#manager.dispose();
|
|
83622
84194
|
await this.#asyncQueue;
|
|
83623
84195
|
this.#subprocess?.close();
|
|
@@ -83713,6 +84285,17 @@ var Thread = class {
|
|
|
83713
84285
|
/** ------------------------------------------------------------------ */
|
|
83714
84286
|
/** Session manager callbacks */
|
|
83715
84287
|
/** ------------------------------------------------------------------ */
|
|
84288
|
+
/**
|
|
84289
|
+
* Dispatches state-change notifications to all registered listeners via
|
|
84290
|
+
* queueMicrotask so listeners cannot synchronously re-enter the session
|
|
84291
|
+
* manager or wasm-backed libraries (Invariant #11).
|
|
84292
|
+
*/
|
|
84293
|
+
#notifyStateChange(newState, prevState) {
|
|
84294
|
+
if (this.#stateChangeListeners.size === 0) return;
|
|
84295
|
+
for (const listener of this.#stateChangeListeners) {
|
|
84296
|
+
queueMicrotask(() => listener(newState, prevState));
|
|
84297
|
+
}
|
|
84298
|
+
}
|
|
83716
84299
|
#handleStatusChange(status) {
|
|
83717
84300
|
this.#config.log({
|
|
83718
84301
|
event: "thread_status_changed",
|
|
@@ -83783,7 +84366,7 @@ ${conversationReplay}` : conversationReplay;
|
|
|
83783
84366
|
threadId: this.#config.threadId,
|
|
83784
84367
|
error: msg
|
|
83785
84368
|
});
|
|
83786
|
-
this.#manager.notifySpawnFailed(msg);
|
|
84369
|
+
this.#manager.notifySpawnFailed(msg, this.#lastSpawnInitialContent ?? void 0);
|
|
83787
84370
|
});
|
|
83788
84371
|
}
|
|
83789
84372
|
async #applySettingsToSubprocess() {
|
|
@@ -84131,9 +84714,13 @@ ${conversationReplay}` : conversationReplay;
|
|
|
84131
84714
|
model: meta?.model ?? null,
|
|
84132
84715
|
reasoningEffort: meta?.reasoningEffort ?? null,
|
|
84133
84716
|
permissionMode: meta?.permissionMode ?? null,
|
|
84134
|
-
...meta?.isSynthetic && { isSynthetic: true }
|
|
84717
|
+
...meta?.isSynthetic && { isSynthetic: true },
|
|
84718
|
+
...meta?.correlationId && { correlationId: meta.correlationId }
|
|
84135
84719
|
});
|
|
84136
84720
|
this.#callbacks.onMessageStored?.(echoSeqNo, echoMsgId, "human", event.sdkUuid);
|
|
84721
|
+
if (meta?.correlationId) {
|
|
84722
|
+
this.#callbacks.onUserMessageConfirmed?.(meta.correlationId, event.sdkUuid);
|
|
84723
|
+
}
|
|
84137
84724
|
break;
|
|
84138
84725
|
}
|
|
84139
84726
|
case "assistant_message": {
|
|
@@ -85463,39 +86050,6 @@ function buildIncrementalMessage(uri, current2, incremental, name, now) {
|
|
|
85463
86050
|
};
|
|
85464
86051
|
}
|
|
85465
86052
|
|
|
85466
|
-
// src/services/roi/inject-shipyard-trailer.ts
|
|
85467
|
-
function buildDefaultTrailerInjectDeps() {
|
|
85468
|
-
return {
|
|
85469
|
-
readCommitMessage: async (cwd) => runWithTimeout("git", ["log", "-1", "--pretty=%B"], cwd, 5e3),
|
|
85470
|
-
amendCommit: async (cwd, message) => {
|
|
85471
|
-
await runWithTimeout("git", ["commit", "--amend", "--no-edit", "-m", message], cwd, 15e3);
|
|
85472
|
-
},
|
|
85473
|
-
getHeadSha: async (cwd) => (await runWithTimeout("git", ["rev-parse", "HEAD"], cwd, 5e3)).trim(),
|
|
85474
|
-
getCurrentBranch: async (cwd) => (await runWithTimeout("git", ["rev-parse", "--abbrev-ref", "HEAD"], cwd, 5e3)).trim(),
|
|
85475
|
-
getRepoSlug: async (cwd) => {
|
|
85476
|
-
const url = (await runWithTimeout("git", ["config", "--get", "remote.origin.url"], cwd, 5e3).catch(
|
|
85477
|
-
() => ""
|
|
85478
|
-
)).trim();
|
|
85479
|
-
const match2 = url.match(/[:/]([^/:]+\/[^/:]+?)(\.git)?\s*$/);
|
|
85480
|
-
return match2?.[1] ?? "";
|
|
85481
|
-
}
|
|
85482
|
-
};
|
|
85483
|
-
}
|
|
85484
|
-
async function injectShipyardTrailer(ctx, deps) {
|
|
85485
|
-
const original = await deps.readCommitMessage(ctx.cwd);
|
|
85486
|
-
if (hasShipyardTrailer(original)) {
|
|
85487
|
-
return { injected: false, reason: "already_trailered" };
|
|
85488
|
-
}
|
|
85489
|
-
const newMessage = appendTrailerToMessage(original, ctx.trailer);
|
|
85490
|
-
await deps.amendCommit(ctx.cwd, newMessage);
|
|
85491
|
-
const [commitSha, branch, repo] = await Promise.all([
|
|
85492
|
-
deps.getHeadSha(ctx.cwd),
|
|
85493
|
-
deps.getCurrentBranch(ctx.cwd),
|
|
85494
|
-
deps.getRepoSlug(ctx.cwd)
|
|
85495
|
-
]);
|
|
85496
|
-
return { injected: true, commitSha, branch, repo };
|
|
85497
|
-
}
|
|
85498
|
-
|
|
85499
86053
|
// src/services/task-resource-resolver.ts
|
|
85500
86054
|
var TASK_URI_PREFIX = "shipyard://task/";
|
|
85501
86055
|
function buildTaskResourceUri(taskId) {
|
|
@@ -88518,17 +89072,6 @@ function trackPlanFileCreation(content, onPlanFile) {
|
|
|
88518
89072
|
}
|
|
88519
89073
|
}
|
|
88520
89074
|
}
|
|
88521
|
-
function detectCommitInBashToolResults(content) {
|
|
88522
|
-
const commitPattern = /(?:^|\n)\[[\w][^[\]]*\s[0-9a-f]{7,40}\]/;
|
|
88523
|
-
const graphitePattern = /Amended.*commit|Committing changes to/i;
|
|
88524
|
-
for (const block2 of content) {
|
|
88525
|
-
if (block2.type !== "tool_result" || block2.isError) continue;
|
|
88526
|
-
if (commitPattern.test(block2.content) || graphitePattern.test(block2.content)) {
|
|
88527
|
-
return true;
|
|
88528
|
-
}
|
|
88529
|
-
}
|
|
88530
|
-
return false;
|
|
88531
|
-
}
|
|
88532
89075
|
|
|
88533
89076
|
// src/services/token-counter.ts
|
|
88534
89077
|
import { execFile as execFile7 } from "child_process";
|
|
@@ -88870,9 +89413,18 @@ function replayTurnStats(lastTurnStats, send) {
|
|
|
88870
89413
|
}
|
|
88871
89414
|
|
|
88872
89415
|
// src/services/task/orchestrator/task.ts
|
|
89416
|
+
function hasActiveCollaborators(participants) {
|
|
89417
|
+
return participants.length > 0;
|
|
89418
|
+
}
|
|
88873
89419
|
function shouldClearPersistence(status) {
|
|
88874
89420
|
return status === "completed" || status === "canceled";
|
|
88875
89421
|
}
|
|
89422
|
+
function classifyRoiTransition(nextStatus, roiEverRan) {
|
|
89423
|
+
if (nextStatus === "completed" || nextStatus === "canceled") return "emit-ended";
|
|
89424
|
+
if (nextStatus === "input_required" && roiEverRan) return "emit-ended";
|
|
89425
|
+
if (nextStatus === "in_progress") return "reset-cycle";
|
|
89426
|
+
return "nothing";
|
|
89427
|
+
}
|
|
88876
89428
|
var Task = class {
|
|
88877
89429
|
#deps;
|
|
88878
89430
|
#mainThread;
|
|
@@ -88914,19 +89466,17 @@ var Task = class {
|
|
|
88914
89466
|
#pendingPostCompactStats = null;
|
|
88915
89467
|
#pendingPostCompactTurnStats = null;
|
|
88916
89468
|
/** ROI: wall-clock start of this task lifetime, used for durationMs on task_ended. */
|
|
88917
|
-
#roiStartedAtMs
|
|
89469
|
+
#roiStartedAtMs;
|
|
88918
89470
|
/** ROI: tracks the most recent status for finalStatus on task_ended. */
|
|
88919
89471
|
#roiLastStatus = "open";
|
|
88920
89472
|
/** ROI: flips true the first time status transitions to in_progress (i.e. agent ran). */
|
|
88921
89473
|
#roiEverRan = false;
|
|
88922
|
-
/** ROI: guard against duplicate task_ended emission. */
|
|
88923
|
-
#
|
|
89474
|
+
/** ROI: guard against duplicate task_ended emission for the current run cycle. */
|
|
89475
|
+
#roiEndedEmittedForCurrentCycle = false;
|
|
88924
89476
|
/** ROI: counts agent turns (incremented on each turn_complete). */
|
|
88925
89477
|
#roiTurnCount = 0;
|
|
88926
|
-
/** ROI: serializes
|
|
89478
|
+
/** ROI: serializes commit-scan triggers so concurrent tool_result_echo events don't race the git amend inside scanAndAttributeCommits. */
|
|
88927
89479
|
#trailerInjectionChain = Promise.resolve();
|
|
88928
|
-
/** ROI: trailer injection deps (overridable for tests). */
|
|
88929
|
-
#trailerInjectDeps = buildDefaultTrailerInjectDeps();
|
|
88930
89480
|
/** MCP status polling (delegated to McpPoller) */
|
|
88931
89481
|
#mcpPoller;
|
|
88932
89482
|
/** Telemetry (delegated to TaskTelemetry) */
|
|
@@ -88949,6 +89499,8 @@ var Task = class {
|
|
|
88949
89499
|
this.#lastTurnStats = deps.initialTurnStats ?? null;
|
|
88950
89500
|
this.#tokenCountResult = deps.initialTokenCount ?? null;
|
|
88951
89501
|
this.#hydrationPromise = deps.hydrationPromise ?? null;
|
|
89502
|
+
this.#roiStartedAtMs = deps.taskCreatedAt;
|
|
89503
|
+
this.#roiTurnCount = deps.initialTurnCount;
|
|
88952
89504
|
this.#telemetry = new TaskTelemetry({
|
|
88953
89505
|
taskId: deps.taskId,
|
|
88954
89506
|
metricsCollector: deps.metricsCollector,
|
|
@@ -88978,10 +89530,16 @@ var Task = class {
|
|
|
88978
89530
|
settings: mergedSettings,
|
|
88979
89531
|
participantId: firstMeta?.participantId ?? this.#deps.humanParticipantId,
|
|
88980
89532
|
senderName: firstMeta?.senderDisplayName ?? this.#deps.ownerDisplayName ?? null,
|
|
88981
|
-
originalContent: content
|
|
89533
|
+
originalContent: content,
|
|
89534
|
+
correlationIds: metadata.map((m2) => m2.correlationId).filter((x2) => !!x2)
|
|
88982
89535
|
});
|
|
88983
89536
|
this.#mainThread.handleUserMessage(content, mergedSettings);
|
|
88984
89537
|
},
|
|
89538
|
+
onBatchForwarded: (correlationIds) => {
|
|
89539
|
+
for (const id of correlationIds) {
|
|
89540
|
+
this.#deps.onForwardedAck(id);
|
|
89541
|
+
}
|
|
89542
|
+
},
|
|
88985
89543
|
log: deps.log,
|
|
88986
89544
|
persistence: deps.collabQueuePersistence,
|
|
88987
89545
|
queueKey: mainQueueKey(deps.taskId)
|
|
@@ -89210,15 +89768,43 @@ var Task = class {
|
|
|
89210
89768
|
* rather than a real user message. Mark it so the browser
|
|
89211
89769
|
* hides it via assembleMessages' isSynthetic filter.
|
|
89212
89770
|
*/
|
|
89213
|
-
isSynthetic: meta === void 0
|
|
89771
|
+
isSynthetic: meta === void 0,
|
|
89772
|
+
correlationId: meta?.correlationIds[0]
|
|
89214
89773
|
};
|
|
89215
89774
|
},
|
|
89775
|
+
onUserMessageConfirmed: (correlationId, sdkUuid) => {
|
|
89776
|
+
const currentMeta = this.#pendingMessageMeta[0];
|
|
89777
|
+
const idsToAck = currentMeta?.correlationIds ?? [correlationId];
|
|
89778
|
+
if (sdkUuid) {
|
|
89779
|
+
const [primary, ...secondary] = idsToAck;
|
|
89780
|
+
const primaryId = primary ?? correlationId;
|
|
89781
|
+
this.#deps.store.stampSdkUuid(
|
|
89782
|
+
this.#deps.channelId,
|
|
89783
|
+
primaryId,
|
|
89784
|
+
sdkUuid,
|
|
89785
|
+
secondary.length > 0 ? secondary : void 0
|
|
89786
|
+
).catch((err) => {
|
|
89787
|
+
this.#deps.log({
|
|
89788
|
+
event: "stamp_sdk_uuid_failed",
|
|
89789
|
+
taskId: this.#deps.taskId,
|
|
89790
|
+
correlationId: primaryId,
|
|
89791
|
+
error: err instanceof Error ? err.message : String(err)
|
|
89792
|
+
});
|
|
89793
|
+
});
|
|
89794
|
+
}
|
|
89795
|
+
for (const id of idsToAck) {
|
|
89796
|
+
this.#deps.onConfirmedAck(id);
|
|
89797
|
+
}
|
|
89798
|
+
},
|
|
89216
89799
|
onBeforeSpawn: (content) => this.#handleBeforeSpawn(content),
|
|
89217
89800
|
onBeforeFlush: (messages) => this.#handleBeforeFlush(messages),
|
|
89218
89801
|
buildCanUseTool: () => this.#buildCanUseTool()
|
|
89219
89802
|
}
|
|
89220
89803
|
);
|
|
89221
89804
|
this.#mainThread.initAdoptedCanUseTool();
|
|
89805
|
+
this.#collabQueue.wireThreadStateSubscription(
|
|
89806
|
+
(listener) => this.#mainThread.onStateChange(listener)
|
|
89807
|
+
);
|
|
89222
89808
|
this.#sideThreads = new SideThreadRegistry({
|
|
89223
89809
|
taskId: deps.taskId,
|
|
89224
89810
|
dataDir: deps.dataDir,
|
|
@@ -89244,13 +89830,22 @@ var Task = class {
|
|
|
89244
89830
|
})
|
|
89245
89831
|
);
|
|
89246
89832
|
}
|
|
89247
|
-
|
|
89248
|
-
|
|
89249
|
-
|
|
89250
|
-
|
|
89251
|
-
|
|
89252
|
-
|
|
89253
|
-
|
|
89833
|
+
if (!deps.initialRoiStartedEmitted) {
|
|
89834
|
+
emitTaskStarted(deps.metricsCollector, {
|
|
89835
|
+
taskId: deps.taskId,
|
|
89836
|
+
userId: deps.userId,
|
|
89837
|
+
activeTaskCount: deps.getActiveTaskCount(),
|
|
89838
|
+
mode: deps.mode,
|
|
89839
|
+
timestamp: this.#roiStartedAtMs
|
|
89840
|
+
});
|
|
89841
|
+
deps.persistRoiStarted().catch((err) => {
|
|
89842
|
+
deps.log({
|
|
89843
|
+
event: "roi_persist_started_failed",
|
|
89844
|
+
taskId: deps.taskId,
|
|
89845
|
+
error: err instanceof Error ? err.message : String(err)
|
|
89846
|
+
});
|
|
89847
|
+
});
|
|
89848
|
+
}
|
|
89254
89849
|
}
|
|
89255
89850
|
get state() {
|
|
89256
89851
|
return this.#mainThread.state;
|
|
@@ -89258,23 +89853,28 @@ var Task = class {
|
|
|
89258
89853
|
get sessionId() {
|
|
89259
89854
|
return this.#mainThread.sessionId;
|
|
89260
89855
|
}
|
|
89856
|
+
/** Current working directory. May be undefined during early bootstrap before EnterWorktree. */
|
|
89857
|
+
get cwd() {
|
|
89858
|
+
return this.#cwd;
|
|
89859
|
+
}
|
|
89261
89860
|
get latestSettings() {
|
|
89262
89861
|
return this.#latestSettings;
|
|
89263
89862
|
}
|
|
89264
89863
|
get mainThread() {
|
|
89265
89864
|
return this.#mainThread;
|
|
89266
89865
|
}
|
|
89267
|
-
handleUserMessage(content, settings, participantId, senderDisplayName) {
|
|
89866
|
+
handleUserMessage(content, settings, participantId, senderDisplayName, correlationId) {
|
|
89268
89867
|
if (settings) {
|
|
89269
89868
|
this.#latestSettings = { ...this.#latestSettings, ...settings };
|
|
89270
89869
|
}
|
|
89271
89870
|
const participants = this.#deps.getCollabParticipants();
|
|
89272
|
-
if (participants
|
|
89871
|
+
if (hasActiveCollaborators(participants)) {
|
|
89273
89872
|
this.#collabQueue.enqueue({
|
|
89274
89873
|
content,
|
|
89275
89874
|
settings,
|
|
89276
89875
|
participantId: participantId ?? this.#deps.humanParticipantId,
|
|
89277
|
-
senderDisplayName: senderDisplayName ?? void 0
|
|
89876
|
+
senderDisplayName: senderDisplayName ?? void 0,
|
|
89877
|
+
correlationId
|
|
89278
89878
|
});
|
|
89279
89879
|
return;
|
|
89280
89880
|
}
|
|
@@ -89282,23 +89882,100 @@ var Task = class {
|
|
|
89282
89882
|
settings: { ...this.#latestSettings },
|
|
89283
89883
|
participantId: participantId ?? this.#deps.humanParticipantId,
|
|
89284
89884
|
senderName: senderDisplayName ?? this.#deps.ownerDisplayName ?? null,
|
|
89285
|
-
originalContent: content
|
|
89885
|
+
originalContent: content,
|
|
89886
|
+
correlationIds: correlationId ? [correlationId] : []
|
|
89286
89887
|
});
|
|
89287
89888
|
this.#mainThread.handleUserMessage(content, settings);
|
|
89889
|
+
if (correlationId) {
|
|
89890
|
+
this.#deps.onForwardedAck(correlationId);
|
|
89891
|
+
}
|
|
89288
89892
|
}
|
|
89289
89893
|
/**
|
|
89290
89894
|
* Restore in-flight collab batches from persistence after daemon
|
|
89291
89895
|
* restart. Replays entries through the main queue so per-peer
|
|
89292
89896
|
* timers re-initialize and missed messages reach the agent on
|
|
89293
89897
|
* the next flush.
|
|
89898
|
+
*
|
|
89899
|
+
* Returns the set of correlationIds whose JSONL rows were claimed by this
|
|
89900
|
+
* replay so rehydrateUnpushedMessages can skip them and avoid double-sending.
|
|
89294
89901
|
*/
|
|
89295
89902
|
async rehydrateCollabQueue() {
|
|
89296
89903
|
const persistence = this.#deps.collabQueuePersistence;
|
|
89297
|
-
if (!persistence) return;
|
|
89904
|
+
if (!persistence) return /* @__PURE__ */ new Set();
|
|
89298
89905
|
const key = mainQueueKey(this.#deps.taskId);
|
|
89299
89906
|
const entries = await persistence.read(key);
|
|
89300
|
-
if (entries.length === 0) return;
|
|
89907
|
+
if (entries.length === 0) return /* @__PURE__ */ new Set();
|
|
89301
89908
|
await this.#collabQueue.rehydrate(entries);
|
|
89909
|
+
const messages = await this.#deps.store.getMessages(this.#deps.channelId);
|
|
89910
|
+
return matchCollabQueueCorrelationIds(messages, entries);
|
|
89911
|
+
}
|
|
89912
|
+
/**
|
|
89913
|
+
* Restore in-flight collab batches for a side thread from persistence
|
|
89914
|
+
* after daemon restart. Mirrors rehydrateCollabQueue for thread-scoped queues.
|
|
89915
|
+
*
|
|
89916
|
+
* Returns false when the side thread is not in the registry — signals orphan
|
|
89917
|
+
* so caller clears the file, mirroring the main-queue orphan-clear path.
|
|
89918
|
+
* Lazily initializes the thread's CollabMessageQueue using the same
|
|
89919
|
+
* construction path as the write side (handleThreadUserMessage).
|
|
89920
|
+
*/
|
|
89921
|
+
async rehydrateThreadQueue(threadId) {
|
|
89922
|
+
const persistence = this.#deps.collabQueuePersistence;
|
|
89923
|
+
if (!persistence || !this.#sideThreads.get(threadId)) return false;
|
|
89924
|
+
const key = threadQueueKey(this.#deps.taskId, threadId);
|
|
89925
|
+
const entries = await persistence.read(key);
|
|
89926
|
+
if (entries.length === 0) return true;
|
|
89927
|
+
let queue = this.#threadQueues.get(threadId);
|
|
89928
|
+
if (!queue) {
|
|
89929
|
+
const sideThread = this.#sideThreads.get(threadId);
|
|
89930
|
+
queue = new CollabMessageQueue({
|
|
89931
|
+
getCollabParticipants: this.#deps.getCollabParticipants,
|
|
89932
|
+
forwardBatch: (batchContent, metadata) => {
|
|
89933
|
+
this.#forwardThreadBatch(threadId, batchContent, metadata);
|
|
89934
|
+
},
|
|
89935
|
+
log: this.#deps.log,
|
|
89936
|
+
persistence: this.#deps.collabQueuePersistence,
|
|
89937
|
+
queueKey: threadQueueKey(this.#deps.taskId, threadId),
|
|
89938
|
+
subscribeToThreadState: sideThread ? (listener) => sideThread.onStateChange(listener) : void 0
|
|
89939
|
+
});
|
|
89940
|
+
this.#threadQueues.set(threadId, queue);
|
|
89941
|
+
}
|
|
89942
|
+
await queue.rehydrate(entries);
|
|
89943
|
+
return true;
|
|
89944
|
+
}
|
|
89945
|
+
/**
|
|
89946
|
+
* Sub-phase 7b: Rehydrate persisted-but-unpushed user messages after daemon restart.
|
|
89947
|
+
*
|
|
89948
|
+
* Scans the JSONL store for rows where correlationId is set but sdkUuid is null
|
|
89949
|
+
* (persisted but not confirmed by SDK). Re-injects them as pushMessage content
|
|
89950
|
+
* so the next subprocess spawn receives them in order.
|
|
89951
|
+
*
|
|
89952
|
+
* Pass excludeCorrelationIds to skip messages already handled by rehydrateCollabQueue,
|
|
89953
|
+
* preventing double-send of the same message to the subprocess.
|
|
89954
|
+
*
|
|
89955
|
+
* Returns the number of messages re-injected.
|
|
89956
|
+
*/
|
|
89957
|
+
async rehydrateUnpushedMessages(excludeCorrelationIds) {
|
|
89958
|
+
const messages = await this.#deps.store.getMessages(this.#deps.channelId);
|
|
89959
|
+
const unpushed = messages.filter(
|
|
89960
|
+
(m2) => m2.senderKind === "human" && m2.correlationId && !m2.sdkUuid && !(excludeCorrelationIds && m2.correlationId && excludeCorrelationIds.has(m2.correlationId))
|
|
89961
|
+
);
|
|
89962
|
+
if (unpushed.length === 0) return 0;
|
|
89963
|
+
for (const msg of unpushed) {
|
|
89964
|
+
this.#pendingMessageMeta.push({
|
|
89965
|
+
settings: { ...this.#latestSettings },
|
|
89966
|
+
participantId: msg.participantId,
|
|
89967
|
+
senderName: null,
|
|
89968
|
+
originalContent: msg.content,
|
|
89969
|
+
correlationIds: msg.correlationId ? [msg.correlationId, ...msg.batchCorrelationIds ?? []] : []
|
|
89970
|
+
});
|
|
89971
|
+
this.#mainThread.handleUserMessage(msg.content);
|
|
89972
|
+
}
|
|
89973
|
+
this.#deps.log({
|
|
89974
|
+
event: "unpushed_messages_rehydrated",
|
|
89975
|
+
taskId: this.#deps.taskId,
|
|
89976
|
+
count: unpushed.length
|
|
89977
|
+
});
|
|
89978
|
+
return unpushed.length;
|
|
89302
89979
|
}
|
|
89303
89980
|
/**
|
|
89304
89981
|
* Route a thread user message through collab micro-batching when
|
|
@@ -89326,7 +90003,8 @@ var Task = class {
|
|
|
89326
90003
|
},
|
|
89327
90004
|
log: this.#deps.log,
|
|
89328
90005
|
persistence: this.#deps.collabQueuePersistence,
|
|
89329
|
-
queueKey: threadQueueKey(this.#deps.taskId, threadId)
|
|
90006
|
+
queueKey: threadQueueKey(this.#deps.taskId, threadId),
|
|
90007
|
+
subscribeToThreadState: (listener) => sideThread.onStateChange(listener)
|
|
89330
90008
|
});
|
|
89331
90009
|
this.#threadQueues.set(threadId, queue);
|
|
89332
90010
|
}
|
|
@@ -89342,7 +90020,8 @@ var Task = class {
|
|
|
89342
90020
|
settings: settings ?? this.#latestSettings,
|
|
89343
90021
|
participantId: participantId ?? this.#deps.humanParticipantId,
|
|
89344
90022
|
senderName: senderDisplayName ?? null,
|
|
89345
|
-
originalContent: content
|
|
90023
|
+
originalContent: content,
|
|
90024
|
+
correlationIds: []
|
|
89346
90025
|
});
|
|
89347
90026
|
sideThread.handleUserMessage(content, settings);
|
|
89348
90027
|
}
|
|
@@ -89855,7 +90534,8 @@ var Task = class {
|
|
|
89855
90534
|
settings: mergedSettings,
|
|
89856
90535
|
participantId: firstMeta?.participantId ?? this.#deps.humanParticipantId,
|
|
89857
90536
|
senderName: firstMeta?.senderDisplayName ?? this.#deps.ownerDisplayName ?? null,
|
|
89858
|
-
originalContent: content
|
|
90537
|
+
originalContent: content,
|
|
90538
|
+
correlationIds: metadata.map((m2) => m2.correlationId).filter((x2) => !!x2)
|
|
89859
90539
|
});
|
|
89860
90540
|
sideThread.handleUserMessage(content, mergedSettings);
|
|
89861
90541
|
}
|
|
@@ -89872,11 +90552,14 @@ var Task = class {
|
|
|
89872
90552
|
if (!arr || arr.length === 0) return void 0;
|
|
89873
90553
|
return arr.shift();
|
|
89874
90554
|
}
|
|
89875
|
-
async #
|
|
90555
|
+
async #awaitHydration() {
|
|
89876
90556
|
if (this.#hydrationPromise) {
|
|
89877
90557
|
await this.#hydrationPromise;
|
|
89878
90558
|
this.#hydrationPromise = null;
|
|
89879
90559
|
}
|
|
90560
|
+
}
|
|
90561
|
+
async #handleBeforeSpawn(initialContent) {
|
|
90562
|
+
await this.#awaitHydration();
|
|
89880
90563
|
await this.#ensureCompactContextFromStore();
|
|
89881
90564
|
if (this.#needsPromotionContext) {
|
|
89882
90565
|
this.#needsPromotionContext = false;
|
|
@@ -89966,6 +90649,7 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
89966
90649
|
return syntheticPrepend.length > 0 ? [...syntheticPrepend, ...annotated] : annotated;
|
|
89967
90650
|
}
|
|
89968
90651
|
async #handleBeforeFlush(messages) {
|
|
90652
|
+
await this.#awaitHydration();
|
|
89969
90653
|
this.#rewindCheckpoint.enqueueSpawnCheckpoint();
|
|
89970
90654
|
const history2 = await this.#deps.store.getMessages(this.#deps.channelId);
|
|
89971
90655
|
const result = [];
|
|
@@ -90005,19 +90689,29 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
90005
90689
|
}
|
|
90006
90690
|
return null;
|
|
90007
90691
|
}
|
|
90008
|
-
|
|
90009
|
-
|
|
90010
|
-
|
|
90692
|
+
/**
|
|
90693
|
+
* Scheduled (cron) tasks have no interactive user — the first agent turn
|
|
90694
|
+
* is the complete run. When the session reaches `input_required` (i.e.
|
|
90695
|
+
* the agent finished and is waiting for the next user message), auto-
|
|
90696
|
+
* complete the task and tear down the session.
|
|
90697
|
+
*
|
|
90698
|
+
* The guard prevents re-entry: stop() triggers stopping -> resumable_idle
|
|
90699
|
+
* which emits another `input_required`, but by then we've already written
|
|
90700
|
+
* `completed`. Subsequent status callbacks after auto-complete are dropped.
|
|
90701
|
+
*
|
|
90702
|
+
* Returns the effective status to use, or null if the caller should return early.
|
|
90703
|
+
*/
|
|
90704
|
+
#resolveScheduledStatus(status) {
|
|
90011
90705
|
if (this.#deps.scheduleId && this.#scheduledAutoCompleted) {
|
|
90012
90706
|
if (status === "in_progress") {
|
|
90013
90707
|
this.#scheduledAutoCompleted = false;
|
|
90014
90708
|
} else {
|
|
90015
|
-
return;
|
|
90709
|
+
return null;
|
|
90016
90710
|
}
|
|
90017
90711
|
}
|
|
90018
90712
|
if (status === "input_required" && this.#deps.scheduleId && !this.#scheduledAutoCompleted) {
|
|
90019
90713
|
this.#scheduledAutoCompleted = true;
|
|
90020
|
-
|
|
90714
|
+
const effective = this.#explicitlyStopped ? "canceled" : "completed";
|
|
90021
90715
|
this.#deps.log({
|
|
90022
90716
|
event: this.#explicitlyStopped ? "scheduled_task_stopped" : "scheduled_task_auto_completed",
|
|
90023
90717
|
taskId: this.#deps.taskId,
|
|
@@ -90026,7 +90720,14 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
90026
90720
|
if (!this.#explicitlyStopped) {
|
|
90027
90721
|
this.#mainThread.stop();
|
|
90028
90722
|
}
|
|
90723
|
+
return effective;
|
|
90029
90724
|
}
|
|
90725
|
+
return status;
|
|
90726
|
+
}
|
|
90727
|
+
#handleStatusChange(status) {
|
|
90728
|
+
if (!isTaskStatus(status)) return;
|
|
90729
|
+
const effectiveStatus = this.#resolveScheduledStatus(status);
|
|
90730
|
+
if (effectiveStatus === null) return;
|
|
90030
90731
|
this.#deps.log({
|
|
90031
90732
|
event: "task_status_changed",
|
|
90032
90733
|
taskId: this.#deps.taskId,
|
|
@@ -90037,6 +90738,19 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
90037
90738
|
status: effectiveStatus
|
|
90038
90739
|
});
|
|
90039
90740
|
this.#trackRoiStatus(effectiveStatus);
|
|
90741
|
+
const roiAction = classifyRoiTransition(effectiveStatus, this.#roiEverRan);
|
|
90742
|
+
switch (roiAction) {
|
|
90743
|
+
case "emit-ended":
|
|
90744
|
+
this.#emitTaskEndedOnce();
|
|
90745
|
+
break;
|
|
90746
|
+
case "reset-cycle":
|
|
90747
|
+
this.#roiEndedEmittedForCurrentCycle = false;
|
|
90748
|
+
break;
|
|
90749
|
+
case "nothing":
|
|
90750
|
+
break;
|
|
90751
|
+
default:
|
|
90752
|
+
assertNever(roiAction);
|
|
90753
|
+
}
|
|
90040
90754
|
this.#deps.writeTaskStatus(effectiveStatus);
|
|
90041
90755
|
if (shouldClearPersistence(effectiveStatus)) {
|
|
90042
90756
|
this.#rewindCheckpoint.cleanup(this.#cwd).catch((err) => {
|
|
@@ -90126,7 +90840,7 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
90126
90840
|
this.#handleWorktreeToolResults(event.content);
|
|
90127
90841
|
this.#subagentManager.handleSubagentToolResults(event.content);
|
|
90128
90842
|
this.#trackPlanFileCreation(event.content);
|
|
90129
|
-
this.#
|
|
90843
|
+
this.#triggerCommitScan();
|
|
90130
90844
|
break;
|
|
90131
90845
|
case "api_usage_snapshot":
|
|
90132
90846
|
this.#lastApiUsage = {
|
|
@@ -90227,6 +90941,13 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
90227
90941
|
}
|
|
90228
90942
|
#handleTurnComplete(event) {
|
|
90229
90943
|
this.#roiTurnCount += 1;
|
|
90944
|
+
this.#deps.onTurnComplete().catch((err) => {
|
|
90945
|
+
this.#deps.log({
|
|
90946
|
+
event: "roi_persist_turn_count_failed",
|
|
90947
|
+
taskId: this.#deps.taskId,
|
|
90948
|
+
error: err instanceof Error ? err.message : String(err)
|
|
90949
|
+
});
|
|
90950
|
+
});
|
|
90230
90951
|
const prevResult = this.#lastTurnResult;
|
|
90231
90952
|
this.#lastTurnResult = event.result;
|
|
90232
90953
|
this.#deps.log({
|
|
@@ -90382,8 +91103,8 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
90382
91103
|
if (status === "in_progress") this.#roiEverRan = true;
|
|
90383
91104
|
}
|
|
90384
91105
|
#emitTaskEndedOnce() {
|
|
90385
|
-
if (this.#
|
|
90386
|
-
this.#
|
|
91106
|
+
if (this.#roiEndedEmittedForCurrentCycle) return;
|
|
91107
|
+
this.#roiEndedEmittedForCurrentCycle = true;
|
|
90387
91108
|
const finalStatus = this.#roiLastStatus === "completed" ? "completed" : this.#roiLastStatus === "input_required" ? "input_required" : "canceled";
|
|
90388
91109
|
emitTaskEnded(this.#deps.metricsCollector, {
|
|
90389
91110
|
taskId: this.#deps.taskId,
|
|
@@ -90398,45 +91119,44 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
90398
91119
|
this.#deps.persistAbandonedAt(Date.now());
|
|
90399
91120
|
}
|
|
90400
91121
|
}
|
|
90401
|
-
#
|
|
90402
|
-
if (!detectCommitInBashToolResults(content)) return;
|
|
91122
|
+
#triggerCommitScan() {
|
|
90403
91123
|
const cwd = this.#cwd;
|
|
90404
91124
|
if (!cwd) return;
|
|
90405
|
-
const sessionId = this.sessionId ?? `pre-session-${this.#deps.taskId}`;
|
|
90406
|
-
const trailer = {
|
|
90407
|
-
version: TRAILER_SCHEMA_VERSION,
|
|
90408
|
-
taskId: this.#deps.taskId,
|
|
90409
|
-
sessionId,
|
|
90410
|
-
model: this.#latestSettings.model ?? "unknown",
|
|
90411
|
-
tokens: this.#costBaseline.totalOutputTokens,
|
|
90412
|
-
costUsd: this.#costBaseline.totalCostUsd,
|
|
90413
|
-
turnCount: this.#roiTurnCount,
|
|
90414
|
-
attributionType: "originated",
|
|
90415
|
-
clientVersion: "shipyard-daemon"
|
|
90416
|
-
};
|
|
90417
91125
|
this.#trailerInjectionChain = this.#trailerInjectionChain.then(async () => {
|
|
90418
91126
|
try {
|
|
90419
|
-
const
|
|
90420
|
-
|
|
90421
|
-
|
|
90422
|
-
|
|
90423
|
-
|
|
90424
|
-
|
|
90425
|
-
|
|
90426
|
-
|
|
90427
|
-
|
|
90428
|
-
|
|
90429
|
-
|
|
90430
|
-
|
|
90431
|
-
|
|
90432
|
-
|
|
91127
|
+
const state = await this.#deps.getCommitScanState();
|
|
91128
|
+
await scanAndAttributeCommits(
|
|
91129
|
+
{
|
|
91130
|
+
cwd,
|
|
91131
|
+
taskId: this.#deps.taskId,
|
|
91132
|
+
userId: this.#deps.userId,
|
|
91133
|
+
lastSeenSha: state.lastSeenSha,
|
|
91134
|
+
attributedCommitShas: state.attributedCommitShas
|
|
91135
|
+
},
|
|
91136
|
+
{
|
|
91137
|
+
metricsCollector: this.#deps.metricsCollector,
|
|
91138
|
+
log: this.#deps.log,
|
|
91139
|
+
amendHead: true,
|
|
91140
|
+
setLastCommitScanSha: (_taskId, sha) => this.#deps.setLastCommitScanSha(sha),
|
|
91141
|
+
addAttributedCommitSha: (_taskId, sha) => this.#deps.addAttributedCommitSha(sha),
|
|
91142
|
+
injectTrailer: injectShipyardTrailer,
|
|
91143
|
+
getAttributionPayload: () => ({
|
|
91144
|
+
model: this.#latestSettings.model ?? "unknown",
|
|
91145
|
+
tokens: this.#costBaseline.totalOutputTokens,
|
|
91146
|
+
costUsd: this.#costBaseline.totalCostUsd,
|
|
91147
|
+
turnCount: this.#roiTurnCount,
|
|
91148
|
+
sessionId: this.sessionId ?? `pre-session-${this.#deps.taskId}`
|
|
91149
|
+
})
|
|
91150
|
+
}
|
|
91151
|
+
);
|
|
90433
91152
|
} catch (err) {
|
|
90434
91153
|
this.#deps.log({
|
|
90435
|
-
event: "
|
|
91154
|
+
event: "roi_commit_scan_failed",
|
|
90436
91155
|
taskId: this.#deps.taskId,
|
|
90437
91156
|
error: err instanceof Error ? err.message : String(err)
|
|
90438
91157
|
});
|
|
90439
91158
|
}
|
|
91159
|
+
}).catch(() => {
|
|
90440
91160
|
});
|
|
90441
91161
|
}
|
|
90442
91162
|
applyOverlay(overlay) {
|
|
@@ -90552,6 +91272,27 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
90552
91272
|
replayTurnStats(this.#lastTurnStats, send);
|
|
90553
91273
|
}
|
|
90554
91274
|
};
|
|
91275
|
+
function matchCollabQueueCorrelationIds(messages, queueEntries) {
|
|
91276
|
+
const unpushedHuman = messages.filter(
|
|
91277
|
+
(m2) => m2.senderKind === "human" && m2.correlationId && !m2.sdkUuid
|
|
91278
|
+
);
|
|
91279
|
+
const claimedIds = /* @__PURE__ */ new Set();
|
|
91280
|
+
let queueIdx = 0;
|
|
91281
|
+
for (const msg of unpushedHuman) {
|
|
91282
|
+
if (queueIdx >= queueEntries.length) break;
|
|
91283
|
+
const entry = queueEntries[queueIdx];
|
|
91284
|
+
if (entry && JSON.stringify(msg.content) === JSON.stringify(entry.content)) {
|
|
91285
|
+
if (msg.correlationId) {
|
|
91286
|
+
claimedIds.add(msg.correlationId);
|
|
91287
|
+
for (const secondary of msg.batchCorrelationIds ?? []) {
|
|
91288
|
+
claimedIds.add(secondary);
|
|
91289
|
+
}
|
|
91290
|
+
}
|
|
91291
|
+
queueIdx++;
|
|
91292
|
+
}
|
|
91293
|
+
}
|
|
91294
|
+
return claimedIds;
|
|
91295
|
+
}
|
|
90555
91296
|
|
|
90556
91297
|
// src/services/task/manager/task-manager-diff.ts
|
|
90557
91298
|
async function rewindTask(params) {
|
|
@@ -90894,6 +91635,35 @@ function findSdkUuidForSeqNoInTask(tasks, taskId, seqNo) {
|
|
|
90894
91635
|
return tasks.get(taskId)?.orchestrator.findSdkUuidForSeqNo(seqNo) ?? null;
|
|
90895
91636
|
}
|
|
90896
91637
|
|
|
91638
|
+
// src/services/task/manager/task-manager-template.ts
|
|
91639
|
+
function buildInitialOverlayFromTemplate(template, now) {
|
|
91640
|
+
if (!template || template.items.length === 0) return void 0;
|
|
91641
|
+
const userTasks = template.items.map((item2) => ({
|
|
91642
|
+
id: item2.id,
|
|
91643
|
+
subject: item2.content,
|
|
91644
|
+
description: item2.description,
|
|
91645
|
+
status: "pending",
|
|
91646
|
+
blocks: [],
|
|
91647
|
+
blockedBy: item2.deps,
|
|
91648
|
+
createdAt: now,
|
|
91649
|
+
updatedAt: now
|
|
91650
|
+
}));
|
|
91651
|
+
return { ...DEFAULT_TASK_OVERLAY, userTasks };
|
|
91652
|
+
}
|
|
91653
|
+
function applyInitialOverlayToOrchestrator(args) {
|
|
91654
|
+
const { taskId, initialOverlay, orchestratorRef, isTaskRegistered } = args;
|
|
91655
|
+
if (orchestratorRef && isTaskRegistered(taskId)) {
|
|
91656
|
+
orchestratorRef.applyOverlay(initialOverlay);
|
|
91657
|
+
args.notifyTaskResourceChange(taskId);
|
|
91658
|
+
return;
|
|
91659
|
+
}
|
|
91660
|
+
args.log({
|
|
91661
|
+
event: "initial_overlay_skipped",
|
|
91662
|
+
taskId,
|
|
91663
|
+
reason: orchestratorRef ? "task_removed" : "orchestrator_not_registered"
|
|
91664
|
+
});
|
|
91665
|
+
}
|
|
91666
|
+
|
|
90897
91667
|
// src/services/task/manager/task-manager.ts
|
|
90898
91668
|
var TaskManager = class {
|
|
90899
91669
|
#deps;
|
|
@@ -90906,6 +91676,18 @@ var TaskManager = class {
|
|
|
90906
91676
|
#onRateLimitEvent = null;
|
|
90907
91677
|
#overlayQueues = /* @__PURE__ */ new Map();
|
|
90908
91678
|
#preWarmManager = null;
|
|
91679
|
+
/**
|
|
91680
|
+
* Per-task confirmed-ack emitter. Set when a message channel channel opens for a task,
|
|
91681
|
+
* cleared when it closes. Called by Task.onConfirmedAck to route 'confirmed' acks back
|
|
91682
|
+
* to the originating peer's message channel handler.
|
|
91683
|
+
*/
|
|
91684
|
+
#confirmedAckEmitters = /* @__PURE__ */ new Map();
|
|
91685
|
+
/**
|
|
91686
|
+
* Per-task forwarded-ack emitter. Set when a message channel opens for a task,
|
|
91687
|
+
* cleared when it closes. Called by Task.onForwardedAck to route 'forwarded' acks
|
|
91688
|
+
* back to the originating peer's message channel handler.
|
|
91689
|
+
*/
|
|
91690
|
+
#forwardedAckEmitters = /* @__PURE__ */ new Map();
|
|
90909
91691
|
constructor(deps) {
|
|
90910
91692
|
this.#deps = deps;
|
|
90911
91693
|
}
|
|
@@ -91102,6 +91884,28 @@ var TaskManager = class {
|
|
|
91102
91884
|
subscribeThreadStreamDelta(taskId, threadId, listener) {
|
|
91103
91885
|
return this.#tasks.get(taskId)?.orchestrator.subscribeSideThreadStreamDelta(threadId, listener) ?? null;
|
|
91104
91886
|
}
|
|
91887
|
+
/**
|
|
91888
|
+
* Register a confirmed-ack emitter for a task's message channel.
|
|
91889
|
+
* Returns an unregister function to call when the channel closes.
|
|
91890
|
+
* When the task's SDK user_message_echo fires for a correlationId,
|
|
91891
|
+
* the emitter is called so the channel can send stage:'confirmed'.
|
|
91892
|
+
*/
|
|
91893
|
+
registerConfirmedAckEmitter(taskId, emitter) {
|
|
91894
|
+
this.#confirmedAckEmitters.set(taskId, emitter);
|
|
91895
|
+
return () => {
|
|
91896
|
+
if (this.#confirmedAckEmitters.get(taskId) === emitter) {
|
|
91897
|
+
this.#confirmedAckEmitters.delete(taskId);
|
|
91898
|
+
}
|
|
91899
|
+
};
|
|
91900
|
+
}
|
|
91901
|
+
registerForwardedAckEmitter(taskId, emitter) {
|
|
91902
|
+
this.#forwardedAckEmitters.set(taskId, emitter);
|
|
91903
|
+
return () => {
|
|
91904
|
+
if (this.#forwardedAckEmitters.get(taskId) === emitter) {
|
|
91905
|
+
this.#forwardedAckEmitters.delete(taskId);
|
|
91906
|
+
}
|
|
91907
|
+
};
|
|
91908
|
+
}
|
|
91105
91909
|
subscribeStreamDelta(taskId, listener) {
|
|
91106
91910
|
const task = this.#tasks.get(taskId);
|
|
91107
91911
|
if (task) {
|
|
@@ -91197,6 +92001,8 @@ var TaskManager = class {
|
|
|
91197
92001
|
if (this.#tasks.has(params.taskId)) return;
|
|
91198
92002
|
const cwd = params.cwd;
|
|
91199
92003
|
const mode = params.mode ?? "task";
|
|
92004
|
+
let orchestratorRef = null;
|
|
92005
|
+
const taskCreatedAt = Date.now();
|
|
91200
92006
|
const storeAndHydrate = async () => {
|
|
91201
92007
|
const template = params.templateId ? await this.#deps.templateStore.get(params.templateId) : null;
|
|
91202
92008
|
if (params.templateId && !template) {
|
|
@@ -91206,26 +92012,23 @@ var TaskManager = class {
|
|
|
91206
92012
|
templateId: params.templateId
|
|
91207
92013
|
});
|
|
91208
92014
|
}
|
|
91209
|
-
const
|
|
91210
|
-
const initialOverlay = template?.items.map((item2) => ({
|
|
91211
|
-
id: item2.id,
|
|
91212
|
-
subject: item2.content,
|
|
91213
|
-
description: item2.description,
|
|
91214
|
-
status: "pending",
|
|
91215
|
-
blocks: [],
|
|
91216
|
-
blockedBy: item2.deps,
|
|
91217
|
-
createdAt: now,
|
|
91218
|
-
updatedAt: now
|
|
91219
|
-
})) ?? [];
|
|
92015
|
+
const initialOverlay = buildInitialOverlayFromTemplate(template, taskCreatedAt);
|
|
91220
92016
|
await this.#deps.taskStateStore.createTask({
|
|
91221
92017
|
...params,
|
|
91222
92018
|
cwd,
|
|
91223
92019
|
mode,
|
|
91224
92020
|
appliedTemplateId: params.templateId,
|
|
91225
|
-
initialOverlay
|
|
92021
|
+
initialOverlay
|
|
91226
92022
|
});
|
|
91227
|
-
if (initialOverlay
|
|
91228
|
-
|
|
92023
|
+
if (initialOverlay) {
|
|
92024
|
+
applyInitialOverlayToOrchestrator({
|
|
92025
|
+
taskId: params.taskId,
|
|
92026
|
+
initialOverlay,
|
|
92027
|
+
orchestratorRef,
|
|
92028
|
+
isTaskRegistered: (id) => this.#tasks.has(id),
|
|
92029
|
+
notifyTaskResourceChange: this.#deps.notifyTaskResourceChange,
|
|
92030
|
+
log: this.#deps.log
|
|
92031
|
+
});
|
|
91229
92032
|
}
|
|
91230
92033
|
};
|
|
91231
92034
|
const hydrationPromise = storeAndHydrate().catch((err) => {
|
|
@@ -91238,14 +92041,28 @@ var TaskManager = class {
|
|
|
91238
92041
|
const claim = mode !== "conversation" ? this.#preWarmManager?.claim(cwd) : void 0;
|
|
91239
92042
|
if (claim) {
|
|
91240
92043
|
claim.subprocess.setHarnessTaskId(params.taskId);
|
|
92044
|
+
const resolved = this.#deps.resolveMcpServers();
|
|
92045
|
+
if (resolved && Object.keys(resolved).length > 0) {
|
|
92046
|
+
claim.subprocess.setMcpServers(resolved).catch((err) => {
|
|
92047
|
+
this.#deps.log({
|
|
92048
|
+
event: "prewarm_adoption_mcp_sync_failed",
|
|
92049
|
+
taskId: params.taskId,
|
|
92050
|
+
error: err instanceof Error ? err.message : String(err)
|
|
92051
|
+
});
|
|
92052
|
+
});
|
|
92053
|
+
}
|
|
91241
92054
|
}
|
|
91242
92055
|
const orchestrator = this.#createTask(params.taskId, params.channelId, {
|
|
91243
92056
|
adoptedSubprocess: claim ?? void 0,
|
|
91244
92057
|
cwd,
|
|
91245
92058
|
mode,
|
|
91246
92059
|
scheduleId: params.scheduleId,
|
|
91247
|
-
hydrationPromise
|
|
92060
|
+
hydrationPromise,
|
|
92061
|
+
initialRoiStartedEmitted: false,
|
|
92062
|
+
taskCreatedAt,
|
|
92063
|
+
initialTurnCount: 0
|
|
91248
92064
|
});
|
|
92065
|
+
orchestratorRef = orchestrator;
|
|
91249
92066
|
this.#tasks.set(params.taskId, {
|
|
91250
92067
|
taskId: params.taskId,
|
|
91251
92068
|
channelId: params.channelId,
|
|
@@ -91273,7 +92090,10 @@ var TaskManager = class {
|
|
|
91273
92090
|
initialTurnStats: opts.initialTurnStats,
|
|
91274
92091
|
initialTokenCount: opts.initialTokenCount,
|
|
91275
92092
|
initialPlanDetection: opts.initialPlanDetection,
|
|
91276
|
-
mode
|
|
92093
|
+
mode,
|
|
92094
|
+
initialRoiStartedEmitted: opts.initialRoiStartedEmitted,
|
|
92095
|
+
taskCreatedAt: opts.taskCreatedAt,
|
|
92096
|
+
initialTurnCount: opts.initialTurnCount
|
|
91277
92097
|
});
|
|
91278
92098
|
this.#tasks.set(taskId, { taskId, channelId, cwd, mode, orchestrator });
|
|
91279
92099
|
this.#flushPendingStreamSubs(taskId, orchestrator);
|
|
@@ -91320,7 +92140,7 @@ var TaskManager = class {
|
|
|
91320
92140
|
getTaskMode(taskId) {
|
|
91321
92141
|
return this.#tasks.get(taskId)?.mode ?? "task";
|
|
91322
92142
|
}
|
|
91323
|
-
async handleUserMessage(taskId, content, settings, cwd, participantId, senderDisplayName) {
|
|
92143
|
+
async handleUserMessage(taskId, content, settings, cwd, participantId, senderDisplayName, correlationId) {
|
|
91324
92144
|
let task = this.#tasks.get(taskId);
|
|
91325
92145
|
if (!task) {
|
|
91326
92146
|
const record = await this.#deps.taskStateStore.getTask(taskId);
|
|
@@ -91337,7 +92157,10 @@ var TaskManager = class {
|
|
|
91337
92157
|
},
|
|
91338
92158
|
initialTurnStats: record.lastTurnStats,
|
|
91339
92159
|
initialTokenCount: record.lastTokenCount,
|
|
91340
|
-
initialPlanDetection: record.lastPlanDetection
|
|
92160
|
+
initialPlanDetection: record.lastPlanDetection,
|
|
92161
|
+
initialRoiStartedEmitted: record.roiStartedEmitted,
|
|
92162
|
+
taskCreatedAt: record.createdAt,
|
|
92163
|
+
initialTurnCount: record.totalTurnCount
|
|
91341
92164
|
});
|
|
91342
92165
|
task = this.#tasks.get(taskId);
|
|
91343
92166
|
if (!task) {
|
|
@@ -91356,7 +92179,13 @@ var TaskManager = class {
|
|
|
91356
92179
|
});
|
|
91357
92180
|
});
|
|
91358
92181
|
}
|
|
91359
|
-
task.orchestrator.handleUserMessage(
|
|
92182
|
+
task.orchestrator.handleUserMessage(
|
|
92183
|
+
content,
|
|
92184
|
+
settings,
|
|
92185
|
+
participantId,
|
|
92186
|
+
senderDisplayName,
|
|
92187
|
+
correlationId
|
|
92188
|
+
);
|
|
91360
92189
|
this.#deps.taskStateStore.bumpActivity(taskId).catch((err) => {
|
|
91361
92190
|
this.#deps.log({
|
|
91362
92191
|
event: "task_state_store_bump_activity_failed",
|
|
@@ -91500,7 +92329,8 @@ var TaskManager = class {
|
|
|
91500
92329
|
return [...this.#tasks.values()].map((t) => ({
|
|
91501
92330
|
taskId: t.taskId,
|
|
91502
92331
|
channelId: t.channelId,
|
|
91503
|
-
sessionState: t.orchestrator.state
|
|
92332
|
+
sessionState: t.orchestrator.state,
|
|
92333
|
+
cwd: t.orchestrator.cwd
|
|
91504
92334
|
}));
|
|
91505
92335
|
}
|
|
91506
92336
|
has(taskId) {
|
|
@@ -91736,7 +92566,35 @@ var TaskManager = class {
|
|
|
91736
92566
|
mode: opts?.mode ?? "task",
|
|
91737
92567
|
scheduleId: opts?.scheduleId,
|
|
91738
92568
|
collabQueuePersistence: this.#deps.collabQueuePersistence,
|
|
91739
|
-
hydrationPromise: opts?.hydrationPromise
|
|
92569
|
+
hydrationPromise: opts?.hydrationPromise,
|
|
92570
|
+
initialRoiStartedEmitted: opts?.initialRoiStartedEmitted ?? false,
|
|
92571
|
+
persistRoiStarted: async () => {
|
|
92572
|
+
await this.#deps.taskStateStore.setRoiStartedEmitted(taskId, { broadcast: false });
|
|
92573
|
+
},
|
|
92574
|
+
taskCreatedAt: opts?.taskCreatedAt ?? Date.now(),
|
|
92575
|
+
initialTurnCount: opts?.initialTurnCount ?? 0,
|
|
92576
|
+
onTurnComplete: async () => {
|
|
92577
|
+
await this.#deps.taskStateStore.incrementTotalTurnCount(taskId, 1, { broadcast: false });
|
|
92578
|
+
},
|
|
92579
|
+
getCommitScanState: async () => {
|
|
92580
|
+
const record = await this.#deps.taskStateStore.getTask(taskId);
|
|
92581
|
+
return {
|
|
92582
|
+
lastSeenSha: record?.lastCommitScanSha ?? null,
|
|
92583
|
+
attributedCommitShas: record?.attributedCommitShas ?? []
|
|
92584
|
+
};
|
|
92585
|
+
},
|
|
92586
|
+
setLastCommitScanSha: async (sha) => {
|
|
92587
|
+
await this.#deps.taskStateStore.setLastCommitScanSha(taskId, sha, { broadcast: false });
|
|
92588
|
+
},
|
|
92589
|
+
addAttributedCommitSha: async (sha) => {
|
|
92590
|
+
await this.#deps.taskStateStore.addAttributedCommitSha(taskId, sha, { broadcast: false });
|
|
92591
|
+
},
|
|
92592
|
+
onConfirmedAck: (correlationId) => {
|
|
92593
|
+
this.#confirmedAckEmitters.get(taskId)?.(correlationId);
|
|
92594
|
+
},
|
|
92595
|
+
onForwardedAck: (correlationId) => {
|
|
92596
|
+
this.#forwardedAckEmitters.get(taskId)?.(correlationId);
|
|
92597
|
+
}
|
|
91740
92598
|
});
|
|
91741
92599
|
}
|
|
91742
92600
|
};
|
|
@@ -91753,6 +92611,37 @@ function applyStatusTransition(task, status, now) {
|
|
|
91753
92611
|
attentionAt: status === "input_required" ? now : null
|
|
91754
92612
|
};
|
|
91755
92613
|
}
|
|
92614
|
+
function arraysEqual(a, b2) {
|
|
92615
|
+
if (a.length !== b2.length) return false;
|
|
92616
|
+
for (let i = 0; i < a.length; i++) {
|
|
92617
|
+
if (a[i] !== b2[i]) return false;
|
|
92618
|
+
}
|
|
92619
|
+
return true;
|
|
92620
|
+
}
|
|
92621
|
+
var structuredTaskEqual = (x2, y) => {
|
|
92622
|
+
if (x2.id !== y.id) return false;
|
|
92623
|
+
if (x2.subject !== y.subject) return false;
|
|
92624
|
+
if (x2.description !== y.description) return false;
|
|
92625
|
+
if (x2.activeForm !== y.activeForm) return false;
|
|
92626
|
+
if (x2.owner !== y.owner) return false;
|
|
92627
|
+
if (x2.status !== y.status) return false;
|
|
92628
|
+
if (x2.createdAt !== y.createdAt) return false;
|
|
92629
|
+
if (x2.updatedAt !== y.updatedAt) return false;
|
|
92630
|
+
if (!arraysEqual(x2.blocks, y.blocks)) return false;
|
|
92631
|
+
if (!arraysEqual(x2.blockedBy, y.blockedBy)) return false;
|
|
92632
|
+
return true;
|
|
92633
|
+
};
|
|
92634
|
+
function structuredTasksEqual(a, b2) {
|
|
92635
|
+
const aKeys = Object.keys(a);
|
|
92636
|
+
if (aKeys.length !== Object.keys(b2).length) return false;
|
|
92637
|
+
for (const id of aKeys) {
|
|
92638
|
+
const x2 = a[id];
|
|
92639
|
+
const y = b2[id];
|
|
92640
|
+
if (!x2 || !y) return false;
|
|
92641
|
+
if (!structuredTaskEqual(x2, y)) return false;
|
|
92642
|
+
}
|
|
92643
|
+
return true;
|
|
92644
|
+
}
|
|
91756
92645
|
function buildTaskStateStore(dataDir) {
|
|
91757
92646
|
const store = buildJsonDocumentStore({
|
|
91758
92647
|
filePath: join45(dataDir, "tasks.json"),
|
|
@@ -91834,6 +92723,11 @@ function buildTaskStateStore(dataDir) {
|
|
|
91834
92723
|
cwd,
|
|
91835
92724
|
totalCostUsd: 0,
|
|
91836
92725
|
totalOutputTokens: 0,
|
|
92726
|
+
totalTurnCount: 0,
|
|
92727
|
+
roiStartedEmitted: false,
|
|
92728
|
+
mergedAt: null,
|
|
92729
|
+
attributedCommitShas: [],
|
|
92730
|
+
lastCommitScanSha: null,
|
|
91837
92731
|
mode: mode ?? "task",
|
|
91838
92732
|
...scheduleId ? { scheduleId } : {},
|
|
91839
92733
|
...scheduleName ? { scheduleName } : {},
|
|
@@ -91895,20 +92789,25 @@ function buildTaskStateStore(dataDir) {
|
|
|
91895
92789
|
);
|
|
91896
92790
|
},
|
|
91897
92791
|
async updateStructuredTasks(taskId, tasks, todoProgress, options) {
|
|
91898
|
-
const now = Date.now();
|
|
91899
92792
|
await safeUpdate(
|
|
91900
92793
|
taskId,
|
|
91901
|
-
(task) =>
|
|
91902
|
-
|
|
91903
|
-
|
|
91904
|
-
|
|
91905
|
-
|
|
91906
|
-
|
|
91907
|
-
|
|
91908
|
-
|
|
91909
|
-
|
|
91910
|
-
|
|
91911
|
-
|
|
92794
|
+
(task) => {
|
|
92795
|
+
const sameTasks = structuredTasksEqual(task.structuredTasks ?? {}, tasks);
|
|
92796
|
+
const sameProgress = !todoProgress || task.todoCompleted === todoProgress.todoCompleted && task.todoTotal === todoProgress.todoTotal && task.currentActivity === todoProgress.currentActivity;
|
|
92797
|
+
if (sameTasks && sameProgress) return task;
|
|
92798
|
+
const now = Date.now();
|
|
92799
|
+
return {
|
|
92800
|
+
...task,
|
|
92801
|
+
structuredTasks: tasks,
|
|
92802
|
+
...todoProgress && {
|
|
92803
|
+
todoCompleted: todoProgress.todoCompleted,
|
|
92804
|
+
todoTotal: todoProgress.todoTotal,
|
|
92805
|
+
currentActivity: todoProgress.currentActivity
|
|
92806
|
+
},
|
|
92807
|
+
updatedAt: now,
|
|
92808
|
+
lastActivityAt: now
|
|
92809
|
+
};
|
|
92810
|
+
},
|
|
91912
92811
|
options
|
|
91913
92812
|
);
|
|
91914
92813
|
},
|
|
@@ -92004,7 +92903,48 @@ function buildTaskStateStore(dataDir) {
|
|
|
92004
92903
|
await safeUpdate(
|
|
92005
92904
|
taskId,
|
|
92006
92905
|
(task) => task.abandonedAt != null ? task : { ...task, abandonedAt: timestamp },
|
|
92007
|
-
{ broadcast: false
|
|
92906
|
+
{ ...options, broadcast: false }
|
|
92907
|
+
);
|
|
92908
|
+
},
|
|
92909
|
+
async setRoiStartedEmitted(taskId, options) {
|
|
92910
|
+
await safeUpdate(
|
|
92911
|
+
taskId,
|
|
92912
|
+
(task) => task.roiStartedEmitted ? task : { ...task, roiStartedEmitted: true },
|
|
92913
|
+
{ ...options, broadcast: false }
|
|
92914
|
+
);
|
|
92915
|
+
},
|
|
92916
|
+
async setMergedAt(taskId, mergedAt, options) {
|
|
92917
|
+
await safeUpdate(taskId, (task) => task.mergedAt != null ? task : { ...task, mergedAt }, {
|
|
92918
|
+
...options,
|
|
92919
|
+
broadcast: false
|
|
92920
|
+
});
|
|
92921
|
+
},
|
|
92922
|
+
async setLastCommitScanSha(taskId, sha, options) {
|
|
92923
|
+
await safeUpdate(
|
|
92924
|
+
taskId,
|
|
92925
|
+
(task) => task.lastCommitScanSha === sha ? task : { ...task, lastCommitScanSha: sha },
|
|
92926
|
+
{ ...options, broadcast: false }
|
|
92927
|
+
);
|
|
92928
|
+
},
|
|
92929
|
+
async addAttributedCommitSha(taskId, sha, options) {
|
|
92930
|
+
await safeUpdate(
|
|
92931
|
+
taskId,
|
|
92932
|
+
(task) => {
|
|
92933
|
+
if (task.attributedCommitShas.includes(sha)) return task;
|
|
92934
|
+
const next = [...task.attributedCommitShas, sha];
|
|
92935
|
+
return {
|
|
92936
|
+
...task,
|
|
92937
|
+
attributedCommitShas: next.length > 50 ? next.slice(next.length - 50) : next
|
|
92938
|
+
};
|
|
92939
|
+
},
|
|
92940
|
+
{ ...options, broadcast: false }
|
|
92941
|
+
);
|
|
92942
|
+
},
|
|
92943
|
+
async incrementTotalTurnCount(taskId, delta = 1, options) {
|
|
92944
|
+
await safeUpdate(
|
|
92945
|
+
taskId,
|
|
92946
|
+
(task) => ({ ...task, totalTurnCount: task.totalTurnCount + delta }),
|
|
92947
|
+
{ ...options, broadcast: false }
|
|
92008
92948
|
);
|
|
92009
92949
|
},
|
|
92010
92950
|
async sweepToInputRequired(taskId, options) {
|
|
@@ -94125,9 +95065,58 @@ async function createDaemon(deps) {
|
|
|
94125
95065
|
const abandonedSweeper = createAbandonedSweeper({
|
|
94126
95066
|
listTasks: () => taskStateStore.listTasks(),
|
|
94127
95067
|
setAbandonedAt: (taskId, timestamp) => taskStateStore.setAbandonedAt(taskId, timestamp),
|
|
95068
|
+
onTaskAbandoned: (taskId, task, abandonedAt) => {
|
|
95069
|
+
if (task.taskStartedAt == null && (task.totalTurnCount ?? 0) === 0) return;
|
|
95070
|
+
emitTaskEnded(deps.metricsCollector, {
|
|
95071
|
+
taskId,
|
|
95072
|
+
userId: deps.auth.userId,
|
|
95073
|
+
finalStatus: "input_required",
|
|
95074
|
+
totalCostUsd: task.totalCostUsd ?? 0,
|
|
95075
|
+
totalOutputTokens: task.totalOutputTokens ?? 0,
|
|
95076
|
+
turnCount: task.totalTurnCount ?? 0,
|
|
95077
|
+
durationMs: Math.max(0, abandonedAt - task.createdAt),
|
|
95078
|
+
timestamp: abandonedAt
|
|
95079
|
+
});
|
|
95080
|
+
},
|
|
94128
95081
|
log: (entry) => deps.log(entry)
|
|
94129
95082
|
});
|
|
94130
95083
|
abandonedSweeper.start();
|
|
95084
|
+
const commitSweepService = new CommitSweepService({
|
|
95085
|
+
metricsCollector: deps.metricsCollector,
|
|
95086
|
+
log: (entry) => deps.log(entry),
|
|
95087
|
+
setLastCommitScanSha: (taskId, sha) => taskStateStore.setLastCommitScanSha(taskId, sha, { broadcast: false }),
|
|
95088
|
+
addAttributedCommitSha: (taskId, sha) => taskStateStore.addAttributedCommitSha(taskId, sha, { broadcast: false }),
|
|
95089
|
+
injectTrailer: injectShipyardTrailer,
|
|
95090
|
+
/**
|
|
95091
|
+
* Periodic sweep never amends HEAD — a zero-cost trailer injected here
|
|
95092
|
+
* would permanently block future in-session scans from attributing the
|
|
95093
|
+
* same commit with real cost (hasShipyardTrailer skips already-trailered
|
|
95094
|
+
* commits). External commits are emitted as 'extended' instead (no
|
|
95095
|
+
* git-amend, attributed for bookkeeping only). See amendHead: false below.
|
|
95096
|
+
*/
|
|
95097
|
+
amendHead: false,
|
|
95098
|
+
getAttributionPayload: (taskId) => ({
|
|
95099
|
+
model: "unknown",
|
|
95100
|
+
tokens: 0,
|
|
95101
|
+
costUsd: 0,
|
|
95102
|
+
turnCount: 0,
|
|
95103
|
+
sessionId: `periodic-sweep-${taskId}`
|
|
95104
|
+
}),
|
|
95105
|
+
getActiveTasks: () => taskManager.listManagedTasks().map(({ taskId, cwd }) => ({
|
|
95106
|
+
taskId,
|
|
95107
|
+
cwd: cwd ?? deps.workspaceRoot,
|
|
95108
|
+
userId: deps.auth.userId
|
|
95109
|
+
})),
|
|
95110
|
+
getTaskAttribution: async (taskId) => {
|
|
95111
|
+
const record = await taskStateStore.getTask(taskId);
|
|
95112
|
+
return {
|
|
95113
|
+
lastSeenSha: record?.lastCommitScanSha ?? null,
|
|
95114
|
+
attributedCommitShas: record?.attributedCommitShas ?? [],
|
|
95115
|
+
userId: deps.auth.userId
|
|
95116
|
+
};
|
|
95117
|
+
}
|
|
95118
|
+
});
|
|
95119
|
+
commitSweepService.start();
|
|
94131
95120
|
const awarenessSampler = createLoroAwarenessSampler({
|
|
94132
95121
|
getRepo: () => repo,
|
|
94133
95122
|
listTaskIds: () => taskManager.listManagedTasks().map((t) => t.taskId),
|
|
@@ -94141,6 +95130,7 @@ async function createDaemon(deps) {
|
|
|
94141
95130
|
async function dispose() {
|
|
94142
95131
|
awarenessSampler.stop();
|
|
94143
95132
|
abandonedSweeper.stop();
|
|
95133
|
+
commitSweepService.stop();
|
|
94144
95134
|
scheduleEvaluator.dispose();
|
|
94145
95135
|
preWarmManager.dispose();
|
|
94146
95136
|
compactor?.dispose();
|
|
@@ -94531,6 +95521,7 @@ function filterOutboundForCollab(msg, collabTaskId) {
|
|
|
94531
95521
|
/** Task-scoped: pass through only if taskId matches */
|
|
94532
95522
|
case "task_state_update":
|
|
94533
95523
|
case "task_removed":
|
|
95524
|
+
case "task_created_ack":
|
|
94534
95525
|
case "permission_request":
|
|
94535
95526
|
case "permission_resolved":
|
|
94536
95527
|
case "pr_action_result":
|
|
@@ -94774,7 +95765,7 @@ function routeMessage(msg, callbacks, log) {
|
|
|
94774
95765
|
);
|
|
94775
95766
|
break;
|
|
94776
95767
|
case "update_settings":
|
|
94777
|
-
callbacks.onUpdateSettings(msg.settings);
|
|
95768
|
+
callbacks.onUpdateSettings(msg.settings, msg.correlationId);
|
|
94778
95769
|
break;
|
|
94779
95770
|
case "update_task_settings":
|
|
94780
95771
|
callbacks.onUpdateTaskSettings(msg.taskId, msg.settings);
|
|
@@ -96356,6 +97347,31 @@ function sendPluginAuthStatusesIfTokens(handler, tokenStore, logAdapter) {
|
|
|
96356
97347
|
});
|
|
96357
97348
|
});
|
|
96358
97349
|
}
|
|
97350
|
+
async function emitTaskCreatedAck(controlHandler, daemon, taskId, templateId, logAdapter) {
|
|
97351
|
+
try {
|
|
97352
|
+
const template = templateId ? await daemon.templateStore.get(templateId) ?? null : null;
|
|
97353
|
+
controlHandler.sendControl({
|
|
97354
|
+
type: "task_created_ack",
|
|
97355
|
+
taskId,
|
|
97356
|
+
templateId: templateId ?? null,
|
|
97357
|
+
appliedTemplate: template ? {
|
|
97358
|
+
templateId: template.id,
|
|
97359
|
+
todos: template.items.map((item2) => ({
|
|
97360
|
+
id: item2.id,
|
|
97361
|
+
content: item2.content,
|
|
97362
|
+
deps: item2.deps
|
|
97363
|
+
}))
|
|
97364
|
+
} : null
|
|
97365
|
+
});
|
|
97366
|
+
} catch (err) {
|
|
97367
|
+
logAdapter({
|
|
97368
|
+
event: "task_created_ack_failed",
|
|
97369
|
+
taskId,
|
|
97370
|
+
templateId: templateId ?? null,
|
|
97371
|
+
error: err instanceof Error ? err.message : String(err)
|
|
97372
|
+
});
|
|
97373
|
+
}
|
|
97374
|
+
}
|
|
96359
97375
|
function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
96360
97376
|
const dc = narrow(rawChannel);
|
|
96361
97377
|
const wsRoot = deps?.workspaceRoot ?? findProjectRoot(process.cwd());
|
|
@@ -96430,11 +97446,128 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
|
96430
97446
|
logAdapter
|
|
96431
97447
|
});
|
|
96432
97448
|
const prAttributedTaskIds = /* @__PURE__ */ new Set();
|
|
97449
|
+
const prMergedTaskIds = /* @__PURE__ */ new Set();
|
|
97450
|
+
const seedReadyPromise = daemon.taskStateStore.listTasks().then((tasks) => {
|
|
97451
|
+
for (const [taskId, record] of Object.entries(tasks)) {
|
|
97452
|
+
if (record.prUrl != null) prAttributedTaskIds.add(taskId);
|
|
97453
|
+
if (record.mergedAt != null) prMergedTaskIds.add(taskId);
|
|
97454
|
+
}
|
|
97455
|
+
}).catch((err) => {
|
|
97456
|
+
logAdapter({
|
|
97457
|
+
event: "pr_sets_seed_failed",
|
|
97458
|
+
error: err instanceof Error ? err.message : String(err)
|
|
97459
|
+
});
|
|
97460
|
+
});
|
|
97461
|
+
const defaultBranchCache2 = /* @__PURE__ */ new Map();
|
|
97462
|
+
async function resolveDefaultBranch2(cwd) {
|
|
97463
|
+
const cached2 = defaultBranchCache2.get(cwd);
|
|
97464
|
+
if (cached2) return cached2;
|
|
97465
|
+
let resolved;
|
|
97466
|
+
try {
|
|
97467
|
+
const branch = await run(
|
|
97468
|
+
"gh",
|
|
97469
|
+
["repo", "view", "--json", "defaultBranchRef", "-q", ".defaultBranchRef.name"],
|
|
97470
|
+
cwd
|
|
97471
|
+
);
|
|
97472
|
+
const trimmed = branch.trim();
|
|
97473
|
+
if (!trimmed) {
|
|
97474
|
+
return "main";
|
|
97475
|
+
}
|
|
97476
|
+
resolved = trimmed;
|
|
97477
|
+
} catch {
|
|
97478
|
+
return "main";
|
|
97479
|
+
}
|
|
97480
|
+
defaultBranchCache2.set(cwd, resolved);
|
|
97481
|
+
return resolved;
|
|
97482
|
+
}
|
|
97483
|
+
async function findCommitByTrailer(cwd, trunk, taskId) {
|
|
97484
|
+
const sha = await run(
|
|
97485
|
+
"git",
|
|
97486
|
+
["log", `origin/${trunk}`, `--grep=Shipyard-Task-Id: ${taskId}`, "-n1", "--pretty=%H"],
|
|
97487
|
+
cwd
|
|
97488
|
+
).catch(() => "");
|
|
97489
|
+
return sha.trim() || null;
|
|
97490
|
+
}
|
|
97491
|
+
async function detectMergeIfNew(pr, taskId, cwd) {
|
|
97492
|
+
if (pr.state === "merged" || pr.mergeCommitSha != null) {
|
|
97493
|
+
return {
|
|
97494
|
+
merged: true,
|
|
97495
|
+
method: "github_native",
|
|
97496
|
+
sha: pr.mergeCommitSha,
|
|
97497
|
+
at: pr.mergedAt ?? Date.now()
|
|
97498
|
+
};
|
|
97499
|
+
}
|
|
97500
|
+
if (pr.state === "closed" && pr.headCommitInBase) {
|
|
97501
|
+
return {
|
|
97502
|
+
merged: true,
|
|
97503
|
+
method: "graphite_queue",
|
|
97504
|
+
sha: pr.headRefSha,
|
|
97505
|
+
at: Date.now()
|
|
97506
|
+
};
|
|
97507
|
+
}
|
|
97508
|
+
if (pr.state === "closed") {
|
|
97509
|
+
const trunk = await resolveDefaultBranch2(cwd);
|
|
97510
|
+
await run("git", ["fetch", "origin", trunk, "--quiet"], cwd, 1e4).catch((err) => {
|
|
97511
|
+
logAdapter({
|
|
97512
|
+
event: "roi_tier3_fetch_failed",
|
|
97513
|
+
taskId,
|
|
97514
|
+
trunk,
|
|
97515
|
+
error: err instanceof Error ? err.message : String(err)
|
|
97516
|
+
});
|
|
97517
|
+
});
|
|
97518
|
+
const sha = await findCommitByTrailer(cwd, trunk, taskId);
|
|
97519
|
+
if (sha) {
|
|
97520
|
+
return { merged: true, method: "graphite_queue", sha, at: Date.now() };
|
|
97521
|
+
}
|
|
97522
|
+
}
|
|
97523
|
+
return { merged: false };
|
|
97524
|
+
}
|
|
96433
97525
|
const prPoller = createPRPoller(
|
|
96434
97526
|
{
|
|
96435
97527
|
onPRState: (payload) => {
|
|
96436
97528
|
controlHandler.sendControl({ type: "pr_state", data: payload });
|
|
96437
|
-
|
|
97529
|
+
seedReadyPromise.then(() => {
|
|
97530
|
+
attributePrIfFirstDiscovery(payload);
|
|
97531
|
+
const pr = payload.currentBranchPR;
|
|
97532
|
+
if (!pr || prMergedTaskIds.has(payload.taskId)) return;
|
|
97533
|
+
const { taskId } = payload;
|
|
97534
|
+
const cwd = prPoller.getCwd(taskId);
|
|
97535
|
+
if (!cwd) return;
|
|
97536
|
+
prMergedTaskIds.add(taskId);
|
|
97537
|
+
detectMergeIfNew(pr, taskId, cwd).then((result) => {
|
|
97538
|
+
if (!result.merged) {
|
|
97539
|
+
prMergedTaskIds.delete(taskId);
|
|
97540
|
+
return;
|
|
97541
|
+
}
|
|
97542
|
+
daemon.taskStateStore.setMergedAt(taskId, result.at).catch((err) => {
|
|
97543
|
+
logAdapter({
|
|
97544
|
+
event: "roi_pr_merged_store_failed",
|
|
97545
|
+
taskId,
|
|
97546
|
+
error: err instanceof Error ? err.message : String(err)
|
|
97547
|
+
});
|
|
97548
|
+
});
|
|
97549
|
+
const remote = parseOwnerRepo(pr.url);
|
|
97550
|
+
const repoKey = remote ? `${remote.owner}/${remote.repo}` : pr.url;
|
|
97551
|
+
emitPrMerged(daemon.metricsCollector, {
|
|
97552
|
+
taskId,
|
|
97553
|
+
userId: daemon.userId,
|
|
97554
|
+
prUrl: pr.url,
|
|
97555
|
+
prNumber: pr.number,
|
|
97556
|
+
repo: repoKey,
|
|
97557
|
+
mergeCommitSha: result.sha,
|
|
97558
|
+
mergeMethod: result.method,
|
|
97559
|
+
mergedAt: result.at
|
|
97560
|
+
});
|
|
97561
|
+
}).catch((err) => {
|
|
97562
|
+
prMergedTaskIds.delete(taskId);
|
|
97563
|
+
logAdapter({
|
|
97564
|
+
event: "roi_pr_merged_detection_failed",
|
|
97565
|
+
taskId,
|
|
97566
|
+
error: err instanceof Error ? err.message : String(err)
|
|
97567
|
+
});
|
|
97568
|
+
});
|
|
97569
|
+
}).catch(() => {
|
|
97570
|
+
});
|
|
96438
97571
|
}
|
|
96439
97572
|
},
|
|
96440
97573
|
prPollerLog
|
|
@@ -96521,7 +97654,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
|
96521
97654
|
senderParticipantId
|
|
96522
97655
|
);
|
|
96523
97656
|
},
|
|
96524
|
-
onUpdateSettings: (settings) => {
|
|
97657
|
+
onUpdateSettings: (settings, correlationId) => {
|
|
96525
97658
|
if (settings.disabledMcpServers && daemon.capabilities.mcpServers) {
|
|
96526
97659
|
const prevDisabled = new Set(
|
|
96527
97660
|
daemon.capabilities.mcpServers.filter((s2) => !s2.enabled).map((s2) => s2.name)
|
|
@@ -96547,7 +97680,11 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
|
96547
97680
|
});
|
|
96548
97681
|
});
|
|
96549
97682
|
}
|
|
96550
|
-
handler.sendControl({
|
|
97683
|
+
handler.sendControl({
|
|
97684
|
+
type: "settings_ack",
|
|
97685
|
+
settings,
|
|
97686
|
+
...correlationId !== void 0 && { correlationId }
|
|
97687
|
+
});
|
|
96551
97688
|
logAdapter({ event: "settings_updated", settings });
|
|
96552
97689
|
},
|
|
96553
97690
|
onUpdateTaskSettings: (taskId, settings) => {
|
|
@@ -96982,6 +98119,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
|
96982
98119
|
},
|
|
96983
98120
|
onCreateTask: (taskId, channelId, title, cwd, mode, templateId) => {
|
|
96984
98121
|
daemon.taskManager.createTask({ taskId, channelId, title, cwd, mode, templateId });
|
|
98122
|
+
void emitTaskCreatedAck(controlHandler, daemon, taskId, templateId, logAdapter);
|
|
96985
98123
|
},
|
|
96986
98124
|
onPromoteTask: (taskId) => {
|
|
96987
98125
|
daemon.taskManager.promoteTask(taskId);
|
|
@@ -97737,7 +98875,7 @@ function createCollabRoomManager(deps) {
|
|
|
97737
98875
|
getParticipantsForTask(taskId) {
|
|
97738
98876
|
for (const room of rooms.values()) {
|
|
97739
98877
|
if (room.taskId === taskId) {
|
|
97740
|
-
return room.participants.map((p2) => ({ name: p2.username, role: p2.role }));
|
|
98878
|
+
return room.participants.filter((p2) => p2.userId !== room.myUserId).map((p2) => ({ name: p2.username, role: p2.role }));
|
|
97741
98879
|
}
|
|
97742
98880
|
}
|
|
97743
98881
|
return [];
|
|
@@ -97998,13 +99136,21 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
97998
99136
|
respond({ type: "error", requestId, error: error2 });
|
|
97999
99137
|
}
|
|
98000
99138
|
function safePath(userPath) {
|
|
99139
|
+
return safePathWithOverrides(userPath, null);
|
|
99140
|
+
}
|
|
99141
|
+
function safePathForRead(userPath) {
|
|
99142
|
+
return safePathWithOverrides(userPath, /* @__PURE__ */ new Set(["node_modules"]));
|
|
99143
|
+
}
|
|
99144
|
+
function safePathWithOverrides(userPath, allowedHiddenNames) {
|
|
98001
99145
|
const normalized = normalize6(userPath);
|
|
98002
99146
|
if (normalized.startsWith("..") || normalized.includes("/..") || normalized.includes("\\..")) {
|
|
98003
99147
|
return null;
|
|
98004
99148
|
}
|
|
98005
99149
|
const segments = normalized.split(/[/\\]/).filter((s2) => s2.length > 0 && s2 !== ".");
|
|
98006
99150
|
for (const seg of segments) {
|
|
98007
|
-
if (isHidden(seg))
|
|
99151
|
+
if (!isHidden(seg)) continue;
|
|
99152
|
+
if (allowedHiddenNames?.has(seg)) continue;
|
|
99153
|
+
return null;
|
|
98008
99154
|
}
|
|
98009
99155
|
const abs = resolve2(cwd, normalized);
|
|
98010
99156
|
const rel = relative3(cwd, abs);
|
|
@@ -98031,7 +99177,8 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
98031
99177
|
function validatePath(msg) {
|
|
98032
99178
|
const abs = safePath(msg.path);
|
|
98033
99179
|
if (!abs) {
|
|
98034
|
-
|
|
99180
|
+
log({ event: "file_io_path_rejected", path: msg.path });
|
|
99181
|
+
respondError(msg.requestId, `hidden_path:${msg.path}`);
|
|
98035
99182
|
return null;
|
|
98036
99183
|
}
|
|
98037
99184
|
return { abs, rel: relative3(cwd, abs) };
|
|
@@ -98063,10 +99210,11 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
98063
99210
|
}
|
|
98064
99211
|
}
|
|
98065
99212
|
function dispatchFileOp(msg) {
|
|
98066
|
-
const
|
|
99213
|
+
const isReadOnlyOp = msg.type === "read_file" || msg.type === "stat";
|
|
99214
|
+
const abs = isReadOnlyOp ? safePathForRead(msg.path) : safePath(msg.path);
|
|
98067
99215
|
if (!abs) {
|
|
98068
99216
|
log({ event: "file_io_path_rejected", path: msg.path });
|
|
98069
|
-
respondError(msg.requestId,
|
|
99217
|
+
respondError(msg.requestId, `hidden_path:${msg.path}`);
|
|
98070
99218
|
return;
|
|
98071
99219
|
}
|
|
98072
99220
|
switch (msg.type) {
|
|
@@ -98232,7 +99380,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
98232
99380
|
}
|
|
98233
99381
|
cwd = canonical;
|
|
98234
99382
|
log({ event: "file_io_cwd_changed", cwd: canonical });
|
|
98235
|
-
respond({ type: "set_cwd_ack", requestId });
|
|
99383
|
+
respond({ type: "set_cwd_ack", requestId, canonical });
|
|
98236
99384
|
for (const listener of cwdChangeListeners) {
|
|
98237
99385
|
listener(canonical);
|
|
98238
99386
|
}
|
|
@@ -98282,13 +99430,19 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
98282
99430
|
try {
|
|
98283
99431
|
const { stdout } = await execFileAsync3("git", args, {
|
|
98284
99432
|
cwd,
|
|
98285
|
-
maxBuffer: 10 * 1024 * 1024
|
|
99433
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
99434
|
+
timeout: 1e4,
|
|
99435
|
+
killSignal: "SIGKILL"
|
|
98286
99436
|
});
|
|
98287
99437
|
const parsed = parseGitGrepOutput(stdout, maxResults);
|
|
99438
|
+
const filteredMatches = parsed.matches.filter((m2) => {
|
|
99439
|
+
const segments = m2.path.split(/[/\\]/).filter((s2) => s2.length > 0 && s2 !== ".");
|
|
99440
|
+
return segments.every((seg) => !isHidden(seg) || seg === "node_modules");
|
|
99441
|
+
});
|
|
98288
99442
|
respond({
|
|
98289
99443
|
type: "git_grep_result",
|
|
98290
99444
|
requestId,
|
|
98291
|
-
matches:
|
|
99445
|
+
matches: filteredMatches,
|
|
98292
99446
|
truncated: parsed.truncated
|
|
98293
99447
|
});
|
|
98294
99448
|
} catch (err) {
|
|
@@ -98838,22 +99992,34 @@ function handleMessageChannel(opts) {
|
|
|
98838
99992
|
peerRole,
|
|
98839
99993
|
senderDisplayName
|
|
98840
99994
|
} = opts;
|
|
98841
|
-
|
|
98842
|
-
const bufferedLiveMessages = [];
|
|
98843
|
-
const unsubscribe = store.subscribe(channelId, (message) => {
|
|
98844
|
-
const outgoing = { type: "message", message };
|
|
98845
|
-
if (catchUpInProgress) {
|
|
98846
|
-
bufferedLiveMessages.push(outgoing);
|
|
98847
|
-
return;
|
|
98848
|
-
}
|
|
98849
|
-
send(JSON.stringify(outgoing));
|
|
98850
|
-
});
|
|
99995
|
+
const inFlightCorrelations = /* @__PURE__ */ new Set();
|
|
98851
99996
|
function sendError(error2) {
|
|
98852
99997
|
send(JSON.stringify({ type: "error", error: error2 }));
|
|
98853
99998
|
}
|
|
98854
99999
|
function formatError2(err) {
|
|
98855
100000
|
return err instanceof Error ? err.message : String(err);
|
|
98856
100001
|
}
|
|
100002
|
+
function sendAck(correlationId, stage, error2) {
|
|
100003
|
+
const ack = error2 ? { type: "send_message_ack", correlationId, stage, error: error2 } : { type: "send_message_ack", correlationId, stage };
|
|
100004
|
+
send(JSON.stringify(ack));
|
|
100005
|
+
}
|
|
100006
|
+
let unregisterConfirmedEmitter = null;
|
|
100007
|
+
if (opts.registerConfirmedAckEmitter) {
|
|
100008
|
+
unregisterConfirmedEmitter = opts.registerConfirmedAckEmitter((correlationId) => {
|
|
100009
|
+
if (inFlightCorrelations.has(correlationId)) {
|
|
100010
|
+
inFlightCorrelations.delete(correlationId);
|
|
100011
|
+
sendAck(correlationId, "confirmed");
|
|
100012
|
+
}
|
|
100013
|
+
});
|
|
100014
|
+
}
|
|
100015
|
+
let unregisterForwardedEmitter = null;
|
|
100016
|
+
if (opts.registerForwardedAckEmitter) {
|
|
100017
|
+
unregisterForwardedEmitter = opts.registerForwardedAckEmitter((correlationId) => {
|
|
100018
|
+
if (inFlightCorrelations.has(correlationId)) {
|
|
100019
|
+
sendAck(correlationId, "forwarded");
|
|
100020
|
+
}
|
|
100021
|
+
});
|
|
100022
|
+
}
|
|
98857
100023
|
function parseAndValidate(data) {
|
|
98858
100024
|
let raw;
|
|
98859
100025
|
try {
|
|
@@ -98873,14 +100039,51 @@ function handleMessageChannel(opts) {
|
|
|
98873
100039
|
}
|
|
98874
100040
|
return result.data;
|
|
98875
100041
|
}
|
|
100042
|
+
let catchUpInProgress = false;
|
|
100043
|
+
const bufferedLiveMessages = [];
|
|
100044
|
+
const unsubscribe = store.subscribe(channelId, (message) => {
|
|
100045
|
+
const outgoing = { type: "message", message };
|
|
100046
|
+
if (catchUpInProgress) {
|
|
100047
|
+
bufferedLiveMessages.push(outgoing);
|
|
100048
|
+
return;
|
|
100049
|
+
}
|
|
100050
|
+
send(JSON.stringify(outgoing));
|
|
100051
|
+
});
|
|
98876
100052
|
async function handleSendMessage2(msg) {
|
|
100053
|
+
const { correlationId } = msg;
|
|
98877
100054
|
try {
|
|
100055
|
+
sendAck(correlationId, "accepted");
|
|
98878
100056
|
const settings = {
|
|
98879
100057
|
model: msg.model,
|
|
98880
100058
|
reasoningEffort: msg.reasoningEffort,
|
|
98881
100059
|
permissionMode: msg.permissionMode,
|
|
98882
100060
|
fastMode: msg.fastMode
|
|
98883
100061
|
};
|
|
100062
|
+
const appendResult = await store.appendMessageDeduped(
|
|
100063
|
+
{
|
|
100064
|
+
messageId: crypto.randomUUID(),
|
|
100065
|
+
channelId,
|
|
100066
|
+
participantId: participantId ?? "human:unknown",
|
|
100067
|
+
senderKind: "human",
|
|
100068
|
+
content: msg.content,
|
|
100069
|
+
timestamp: Date.now(),
|
|
100070
|
+
correlationId,
|
|
100071
|
+
model: settings.model ?? null,
|
|
100072
|
+
reasoningEffort: settings.reasoningEffort ?? null,
|
|
100073
|
+
permissionMode: settings.permissionMode ?? null
|
|
100074
|
+
},
|
|
100075
|
+
{}
|
|
100076
|
+
);
|
|
100077
|
+
if (appendResult.isDuplicate) {
|
|
100078
|
+
log({
|
|
100079
|
+
event: "jsonl_dedup_correlation",
|
|
100080
|
+
taskId,
|
|
100081
|
+
correlationId,
|
|
100082
|
+
dedupKey: appendResult.dedupKey
|
|
100083
|
+
});
|
|
100084
|
+
}
|
|
100085
|
+
sendAck(correlationId, "persisted");
|
|
100086
|
+
inFlightCorrelations.add(correlationId);
|
|
98884
100087
|
if (opts.onUserMessage) {
|
|
98885
100088
|
opts.onUserMessage(msg.content, settings, msg.cwd, participantId, senderDisplayName);
|
|
98886
100089
|
} else {
|
|
@@ -98890,12 +100093,14 @@ function handleMessageChannel(opts) {
|
|
|
98890
100093
|
settings,
|
|
98891
100094
|
msg.cwd,
|
|
98892
100095
|
participantId,
|
|
98893
|
-
senderDisplayName
|
|
100096
|
+
senderDisplayName,
|
|
100097
|
+
correlationId
|
|
98894
100098
|
);
|
|
98895
100099
|
}
|
|
98896
100100
|
} catch (err) {
|
|
98897
|
-
|
|
98898
|
-
|
|
100101
|
+
sendAck(correlationId, "rejected", formatError2(err));
|
|
100102
|
+
inFlightCorrelations.delete(correlationId);
|
|
100103
|
+
log({ event: "message_handler_error", taskId, correlationId, error: formatError2(err) });
|
|
98899
100104
|
}
|
|
98900
100105
|
}
|
|
98901
100106
|
function flushBufferedMessages() {
|
|
@@ -98905,8 +100110,48 @@ function handleMessageChannel(opts) {
|
|
|
98905
100110
|
}
|
|
98906
100111
|
bufferedLiveMessages.length = 0;
|
|
98907
100112
|
}
|
|
100113
|
+
function classifyCorrelationStatus(cid, persisted) {
|
|
100114
|
+
const stored = persisted.get(cid);
|
|
100115
|
+
if (stored === "confirmed") return "confirmed";
|
|
100116
|
+
if (stored === "persisted") return inFlightCorrelations.has(cid) ? "forwarded" : "persisted";
|
|
100117
|
+
return "unknown";
|
|
100118
|
+
}
|
|
100119
|
+
function buildPersistedMap(messages) {
|
|
100120
|
+
const persisted = /* @__PURE__ */ new Map();
|
|
100121
|
+
for (const m2 of messages) {
|
|
100122
|
+
if (m2.correlationId) {
|
|
100123
|
+
const status = m2.sdkUuid ? "confirmed" : "persisted";
|
|
100124
|
+
persisted.set(m2.correlationId, status);
|
|
100125
|
+
for (const secondary of m2.batchCorrelationIds ?? []) {
|
|
100126
|
+
persisted.set(secondary, status);
|
|
100127
|
+
}
|
|
100128
|
+
}
|
|
100129
|
+
}
|
|
100130
|
+
return persisted;
|
|
100131
|
+
}
|
|
100132
|
+
function sendCorrelationSnapshot(inFlightIds, persisted) {
|
|
100133
|
+
const entries = inFlightIds.map((cid) => ({
|
|
100134
|
+
correlationId: cid,
|
|
100135
|
+
status: classifyCorrelationStatus(cid, persisted)
|
|
100136
|
+
}));
|
|
100137
|
+
const snapshot = { type: "correlation_status_snapshot", entries };
|
|
100138
|
+
send(JSON.stringify(snapshot));
|
|
100139
|
+
}
|
|
98908
100140
|
function handleSubscribe(msg) {
|
|
98909
100141
|
catchUpInProgress = true;
|
|
100142
|
+
if (msg.inFlightCorrelationIds && msg.inFlightCorrelationIds.length > 0) {
|
|
100143
|
+
const inFlightIds = msg.inFlightCorrelationIds;
|
|
100144
|
+
store.getMessages(channelId).then((messages) => {
|
|
100145
|
+
const persisted = buildPersistedMap(messages);
|
|
100146
|
+
for (const cid of inFlightIds) {
|
|
100147
|
+
if (persisted.get(cid) === "persisted") {
|
|
100148
|
+
inFlightCorrelations.add(cid);
|
|
100149
|
+
}
|
|
100150
|
+
}
|
|
100151
|
+
sendCorrelationSnapshot(inFlightIds, persisted);
|
|
100152
|
+
}).catch(() => {
|
|
100153
|
+
});
|
|
100154
|
+
}
|
|
98910
100155
|
if (msg.maxMessages && msg.sinceSeqNo === 0) {
|
|
98911
100156
|
const limit = msg.maxMessages;
|
|
98912
100157
|
store.getMessagesBefore(channelId, Number.POSITIVE_INFINITY, limit).then(({ messages: page }) => {
|
|
@@ -99040,6 +100285,11 @@ function handleMessageChannel(opts) {
|
|
|
99040
100285
|
sendStreamDelta,
|
|
99041
100286
|
dispose() {
|
|
99042
100287
|
unsubscribe();
|
|
100288
|
+
unregisterConfirmedEmitter?.();
|
|
100289
|
+
unregisterConfirmedEmitter = null;
|
|
100290
|
+
unregisterForwardedEmitter?.();
|
|
100291
|
+
unregisterForwardedEmitter = null;
|
|
100292
|
+
inFlightCorrelations.clear();
|
|
99043
100293
|
}
|
|
99044
100294
|
};
|
|
99045
100295
|
}
|
|
@@ -99566,6 +100816,10 @@ function attachConversationHandler(daemon, dc, channelId, params, log, attempts)
|
|
|
99566
100816
|
taskManager: daemon.taskManager,
|
|
99567
100817
|
store: daemon.store,
|
|
99568
100818
|
log,
|
|
100819
|
+
...!threadId && {
|
|
100820
|
+
registerConfirmedAckEmitter: (emitter) => daemon.taskManager.registerConfirmedAckEmitter(taskId, emitter),
|
|
100821
|
+
registerForwardedAckEmitter: (emitter) => daemon.taskManager.registerForwardedAckEmitter(taskId, emitter)
|
|
100822
|
+
},
|
|
99569
100823
|
...peer && {
|
|
99570
100824
|
participantId: peer.participantId,
|
|
99571
100825
|
peerRole: peer.peerRole,
|
|
@@ -101706,7 +102960,9 @@ async function serve(options = {}) {
|
|
|
101706
102960
|
const shipyardHome = options.shipyardHome ?? getShipyardHome();
|
|
101707
102961
|
const dataDir = join55(shipyardHome, options.isDev ? "data-dev" : "data");
|
|
101708
102962
|
const log = createChildLogger({ mode: "serve" });
|
|
101709
|
-
const workspaceRoot = findProjectRoot(process.cwd())
|
|
102963
|
+
const workspaceRoot = await realpath2(findProjectRoot(process.cwd())).catch(
|
|
102964
|
+
() => findProjectRoot(process.cwd())
|
|
102965
|
+
);
|
|
101710
102966
|
registerBuiltinPlugins();
|
|
101711
102967
|
const pluginConfigStore = buildPluginConfigStore(join55(dataDir, "plugins"));
|
|
101712
102968
|
await mkdir24(dataDir, { recursive: true });
|
|
@@ -102048,4 +103304,4 @@ export {
|
|
|
102048
103304
|
_testing,
|
|
102049
103305
|
serve
|
|
102050
103306
|
};
|
|
102051
|
-
//# sourceMappingURL=serve-
|
|
103307
|
+
//# sourceMappingURL=serve-P5WC5JIT.js.map
|