@bojackduy/opencode-loopd 1.9.7 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/server.js +355 -50
  2. package/dist/tui.js +705 -490
  3. package/package.json +2 -1
package/dist/server.js CHANGED
@@ -487,7 +487,7 @@ var CURRENT_VERSION = 6, LOCK_STALE_MS = 1e4;
487
487
  var init_state_repository = () => {};
488
488
 
489
489
  // src/server/plugin.ts
490
- import { define } from "@opencode-ai/plugin/v2/promise";
490
+ import { tool as v1Tool } from "@opencode-ai/plugin/tool";
491
491
 
492
492
  // src/application/control-worker.ts
493
493
  init_state_repository();
@@ -1639,6 +1639,9 @@ function createLoopEngine(options) {
1639
1639
  }
1640
1640
  return true;
1641
1641
  }
1642
+ function hasExecutionLeak(runtime) {
1643
+ return runtime.phase !== "idle" || Boolean(runtime.activeRunID) || Boolean(runtime.leaseExpiresAt) || Boolean(runtime.turnStartedAt) || Boolean(runtime.activePromptMessageID) || Boolean(runtime.activePromptObservedAt) || Boolean(runtime.activeAssistantMessageID) || Boolean(runtime.activeAssistantCompletedAt) || Boolean(runtime.idleCandidateAt) || (runtime.activeToolCallIDs?.length ?? 0) > 0;
1644
+ }
1642
1645
  async function maintenance() {
1643
1646
  syncWorkerSessionsFromService();
1644
1647
  if (knownWorkerSessions.size === 0)
@@ -1650,10 +1653,11 @@ function createLoopEngine(options) {
1650
1653
  const rt = state.runtimes.find((r) => r.goalID === goal.id);
1651
1654
  if (!rt)
1652
1655
  continue;
1653
- if (rt.phase === "idle" && (rt.activeRunID || rt.leaseExpiresAt || rt.activePromptMessageID)) {
1656
+ if (hasExecutionLeak(rt)) {
1654
1657
  await mutateState(directory, `maintenance.clear-terminal-leak:${goal.id}`, async (s) => {
1655
1658
  const r = s.runtimes.find((x) => x.goalID === goal.id);
1656
- if (r && r.phase === "idle" && (r.activeRunID || r.leaseExpiresAt || r.activePromptMessageID)) {
1659
+ const g = s.goals.find((item) => item.id === goal.id);
1660
+ if (g?.status !== "active" && r && hasExecutionLeak(r)) {
1657
1661
  Object.assign(r, releaseLease(r));
1658
1662
  r.activeRunID = undefined;
1659
1663
  r.unknownStatusCount = 0;
@@ -2103,6 +2107,115 @@ function createRealHost(client, directory) {
2103
2107
  }
2104
2108
  };
2105
2109
  }
2110
+ function createV2Host(context, statuses) {
2111
+ const directory = context.location.directory;
2112
+ return {
2113
+ async createWorker({ parentID, title, agent, model }) {
2114
+ const session = await context.session.create({
2115
+ title,
2116
+ agent,
2117
+ model: model ? { id: model.modelID, providerID: model.providerID } : undefined,
2118
+ location: { directory },
2119
+ metadata: { "loopd.parentID": parentID }
2120
+ });
2121
+ statuses.set(session.id, "idle");
2122
+ await logServerEvent(directory, "worker.created", { parentID, workerSessionID: session.id, title });
2123
+ return session.id;
2124
+ },
2125
+ async promptWorker({ sessionID, prompt, messageID, model, agent }) {
2126
+ if (agent)
2127
+ await context.session.switchAgent({ sessionID, agent });
2128
+ if (model) {
2129
+ await context.session.switchModel({
2130
+ sessionID,
2131
+ model: { id: model.modelID, providerID: model.providerID }
2132
+ });
2133
+ }
2134
+ const result = await context.session.prompt({
2135
+ sessionID,
2136
+ id: messageID,
2137
+ text: prompt
2138
+ });
2139
+ statuses.set(sessionID, "busy");
2140
+ await logServerEvent(directory, "worker.prompted", { sessionID });
2141
+ return { messageID: result.id };
2142
+ },
2143
+ async readSession(sessionID) {
2144
+ try {
2145
+ const session = await context.session.get({ sessionID });
2146
+ const model = session.model ? { providerID: session.model.providerID, modelID: session.model.id } : undefined;
2147
+ if (!session.agent && !model)
2148
+ return;
2149
+ return { agent: session.agent, model };
2150
+ } catch {
2151
+ return;
2152
+ }
2153
+ },
2154
+ async sessionStatus(sessionID) {
2155
+ return statuses.get(sessionID) ?? "unknown";
2156
+ },
2157
+ async abortSession(sessionID) {
2158
+ try {
2159
+ await context.session.interrupt({ sessionID });
2160
+ statuses.set(sessionID, "idle");
2161
+ } catch {}
2162
+ },
2163
+ async readMessages(sessionID, limit = 10) {
2164
+ try {
2165
+ const messages = await context.session.context({ sessionID });
2166
+ let parentMessageID;
2167
+ return messages.flatMap((message) => {
2168
+ if (message.type === "user") {
2169
+ parentMessageID = message.id;
2170
+ return [{
2171
+ role: "user",
2172
+ content: message.text,
2173
+ timestamp: new Date(message.time.created).toISOString(),
2174
+ messageID: message.id
2175
+ }];
2176
+ }
2177
+ if (message.type !== "assistant")
2178
+ return [];
2179
+ const created = message.time.created;
2180
+ const completed = message.time.completed;
2181
+ return [{
2182
+ role: "assistant",
2183
+ content: message.content.filter((part) => part.type === "text").map((part) => part.text).join(`
2184
+ `),
2185
+ timestamp: new Date(completed ?? created).toISOString(),
2186
+ messageID: message.id,
2187
+ parentMessageID,
2188
+ completedAt: completed ? new Date(completed).toISOString() : undefined,
2189
+ tokens: message.tokens ? {
2190
+ input: message.tokens.input,
2191
+ output: message.tokens.output,
2192
+ reasoning: message.tokens.reasoning,
2193
+ cacheRead: message.tokens.cache.read,
2194
+ cacheWrite: message.tokens.cache.write
2195
+ } : undefined,
2196
+ cost: message.cost,
2197
+ durationMs: completed && completed >= created ? completed - created : undefined
2198
+ }];
2199
+ }).slice(-limit);
2200
+ } catch {
2201
+ return [];
2202
+ }
2203
+ },
2204
+ async compactSession(sessionID) {
2205
+ try {
2206
+ await context.session.command({ sessionID, name: "compact", text: "" });
2207
+ } catch {}
2208
+ },
2209
+ async notifyOwner(ownerSessionID, message) {
2210
+ try {
2211
+ await context.session.prompt({ sessionID: ownerSessionID, text: message });
2212
+ await logServerEvent(directory, "parent.notified", { ownerSessionID, preview: message.slice(0, 160) });
2213
+ } catch (error) {
2214
+ await logServerEvent(directory, "parent.notify.failed", { ownerSessionID, detail: describeError(error) });
2215
+ }
2216
+ }
2217
+ };
2218
+ }
2106
2219
  async function withTimeout(promise, timeoutMs, operation) {
2107
2220
  let timer;
2108
2221
  try {
@@ -2785,9 +2898,13 @@ function createGoalService(host) {
2785
2898
  runtime.consecutiveFailures = 0;
2786
2899
  runtime.lastError = undefined;
2787
2900
  runtime.forceFinishRequested = undefined;
2901
+ runtime.evaluatorRejectionCount = 0;
2902
+ runtime.lastRejectionDetails = undefined;
2903
+ runtime.freeRetryPending = false;
2788
2904
  runtime.lastParentNotifiedAt = undefined;
2789
2905
  runtime.lastParentNotifiedFor = undefined;
2790
- runtime.phase = "idle";
2906
+ Object.assign(runtime, releaseLease(runtime));
2907
+ runtime.activeRunID = undefined;
2791
2908
  runtime.updatedAt = new Date().toISOString();
2792
2909
  }
2793
2910
  return state;
@@ -3419,25 +3536,32 @@ function goalTools(dir, goalService, hostSessionID, defaults = {}, host) {
3419
3536
  const cwd = goal.config.checkCwd || goal.config.artifactDir || dir;
3420
3537
  const checkResults = await runCompletionChecks(goal.config.checks, cwd);
3421
3538
  if (!checkResults.passed) {
3422
- const runtime = state.runtimes.find((r) => r.goalID === goal.id);
3423
- if (runtime) {
3424
- runtime.evaluatorRejectionCount = (runtime.evaluatorRejectionCount || 0) + 1;
3425
- const failureDetails = checkResults.failures.map((f) => {
3426
- const stdoutSnippet = f.stdout ? `
3539
+ const failureDetails = checkResults.failures.map((f) => {
3540
+ const stdoutSnippet = f.stdout ? `
3427
3541
  Stdout: ${f.stdout.slice(0, 500)}` : "";
3428
- const stderrSnippet = f.stderr ? `
3542
+ const stderrSnippet = f.stderr ? `
3429
3543
  Stderr: ${f.stderr.slice(0, 500)}` : "";
3430
- return `Command: ${f.command}
3544
+ return `Command: ${f.command}
3431
3545
  Exit code: ${f.exitCode}${stdoutSnippet}${stderrSnippet}`;
3432
- }).join(`
3546
+ }).join(`
3433
3547
 
3434
3548
  `);
3549
+ let rejectionCount = 0;
3550
+ let blocked = false;
3551
+ let attemptID = "";
3552
+ const rejectionState = await mutateState(dir, `goal.completion-rejected:${goal.id}`, async (current) => {
3553
+ const currentGoal = current.goals.find((item) => item.id === goal.id);
3554
+ const runtime = current.runtimes.find((r) => r.goalID === goal.id);
3555
+ if (!currentGoal || !runtime || currentGoal.status !== "active")
3556
+ return current;
3557
+ runtime.evaluatorRejectionCount = (runtime.evaluatorRejectionCount || 0) + 1;
3558
+ rejectionCount = runtime.evaluatorRejectionCount;
3435
3559
  runtime.lastRejectionDetails = `Rejection #${runtime.evaluatorRejectionCount} at ${new Date().toISOString()}
3436
3560
 
3437
3561
  Working directory: ${cwd}
3438
3562
 
3439
3563
  ${failureDetails}`;
3440
- const attemptID = randomUUID5();
3564
+ attemptID = randomUUID5();
3441
3565
  const verificationAttempt = {
3442
3566
  id: attemptID,
3443
3567
  sequence: runtime.evaluatorRejectionCount,
@@ -3457,55 +3581,71 @@ ${failureDetails}`;
3457
3581
  };
3458
3582
  runtime.lastVerificationAttempt = verificationAttempt;
3459
3583
  runtime.recentVerificationAttempts = appendVerificationAttempt(runtime.recentVerificationAttempts || [], verificationAttempt);
3460
- const rejectEvent = {
3461
- version: 1,
3462
- eventID: randomUUID5(),
3463
- goalID: goal.id,
3464
- type: "goal.completion_rejected",
3465
- attemptID,
3466
- rejectionCount: runtime.evaluatorRejectionCount,
3467
- failedCheckCount: checkResults.failures.length,
3468
- failureSummary: failureDetails.slice(0, 500),
3469
- timestamp: new Date().toISOString(),
3470
- revision: state.revision
3471
- };
3472
- await appendEvent(dir, rejectEvent);
3473
- const maxRejections = goal.config.maxEvaluatorRejections || 3;
3584
+ const maxRejections = currentGoal.config.maxEvaluatorRejections || 3;
3474
3585
  if (runtime.evaluatorRejectionCount >= maxRejections) {
3475
- goal.status = "blocked";
3476
- goal.updatedAt = new Date().toISOString();
3477
- goal.blocker = {
3586
+ blocked = true;
3587
+ currentGoal.status = "blocked";
3588
+ currentGoal.updatedAt = new Date().toISOString();
3589
+ currentGoal.blocker = {
3478
3590
  reason: `Evaluator rejected ${runtime.evaluatorRejectionCount} time(s). Last failure:
3479
3591
  ${failureDetails.slice(0, 500)}`,
3480
3592
  needed: "Fix the failing checks and retry the goal.",
3481
3593
  at: new Date().toISOString()
3482
3594
  };
3595
+ Object.assign(runtime, releaseLease(runtime));
3596
+ runtime.activeRunID = undefined;
3483
3597
  runtime.forceFinishRequested = undefined;
3484
- await appendEvent(dir, {
3485
- version: 1,
3486
- eventID: randomUUID5(),
3487
- goalID: goal.id,
3488
- type: "goal.blocked",
3489
- reason: goal.blocker.reason,
3490
- needed: goal.blocker.needed,
3491
- timestamp: new Date().toISOString(),
3492
- revision: state.revision
3493
- });
3598
+ runtime.freeRetryPending = false;
3494
3599
  } else {
3495
3600
  runtime.forceFinishRequested = false;
3496
3601
  runtime.freeRetryPending = true;
3497
3602
  }
3498
3603
  runtime.updatedAt = new Date().toISOString();
3499
- await writeState(dir, state);
3604
+ return current;
3605
+ });
3606
+ if (attemptID) {
3607
+ await appendEvent(dir, {
3608
+ version: 1,
3609
+ eventID: randomUUID5(),
3610
+ goalID: goal.id,
3611
+ type: "goal.completion_rejected",
3612
+ attemptID,
3613
+ rejectionCount,
3614
+ failedCheckCount: checkResults.failures.length,
3615
+ failureSummary: failureDetails.slice(0, 500),
3616
+ timestamp: new Date().toISOString(),
3617
+ revision: rejectionState.revision
3618
+ });
3619
+ }
3620
+ const rejectedGoal = rejectionState.goals.find((item) => item.id === goal.id);
3621
+ const rejectedRuntime = rejectionState.runtimes.find((item) => item.goalID === goal.id);
3622
+ rejectionCount = rejectedRuntime?.evaluatorRejectionCount ?? rejectionCount;
3623
+ const goalIsBlocked = rejectedGoal?.status === "blocked";
3624
+ if (blocked && rejectedGoal?.blocker) {
3625
+ await appendEvent(dir, {
3626
+ version: 1,
3627
+ eventID: randomUUID5(),
3628
+ goalID: goal.id,
3629
+ type: "goal.blocked",
3630
+ reason: rejectedGoal.blocker.reason,
3631
+ needed: rejectedGoal.blocker.needed,
3632
+ timestamp: new Date().toISOString(),
3633
+ revision: rejectionState.revision
3634
+ });
3635
+ await goalService.accountUsage(dir, goal.id).catch(() => {
3636
+ return;
3637
+ });
3500
3638
  }
3501
3639
  return {
3502
- title: "Completion rejected \u2014 keep working",
3640
+ title: goalIsBlocked ? "Completion rejected \u2014 goal blocked" : "Completion rejected \u2014 keep working",
3503
3641
  output: JSON.stringify({
3642
+ goalID: goal.id,
3643
+ goalName: goal.name,
3504
3644
  passed: false,
3505
3645
  failedChecks: checkResults.failures,
3506
- message: "Evaluator rejected completion. Fix the issues above and try again.",
3507
- rejectionCount: runtime?.evaluatorRejectionCount || 0,
3508
- status: goal.status
3646
+ message: goalIsBlocked ? "Evaluator rejection limit reached. Goal blocked; owner retry required." : "Evaluator rejected completion. Fix the issues above and try again.",
3647
+ rejectionCount,
3648
+ status: rejectedGoal?.status ?? goal.status
3509
3649
  })
3510
3650
  };
3511
3651
  }
@@ -3772,6 +3912,42 @@ async function runCompletionChecks(checks, cwd) {
3772
3912
  // src/server/owner-tools.ts
3773
3913
  init_state_repository();
3774
3914
  import { tool as tool2 } from "@opencode-ai/plugin/tool";
3915
+
3916
+ // src/domain/status-labels.ts
3917
+ var GOAL_STATUS_META = {
3918
+ active: { short: "Active", hint: "loopd owns it" },
3919
+ paused: { short: "Paused", hint: "stopped by you" },
3920
+ blocked: { short: "Blocked", hint: "needs you" },
3921
+ budget_limited: { short: "Out of budget", hint: "resume to spend" },
3922
+ usage_limited: { short: "Waiting for capacity", hint: "auto-resumes" },
3923
+ complete: { short: "Done", hint: "verified" }
3924
+ };
3925
+ var PHASE_META = {
3926
+ idle: { short: "Idle", hint: "between turns" },
3927
+ queued: { short: "Queued", hint: "waiting to start" },
3928
+ running: { short: "Running", hint: "worker acting now" },
3929
+ compacting: { short: "Compacting", hint: "summarizing context" },
3930
+ waiting_retry: { short: "Retrying", hint: "backing off" },
3931
+ stopping: { short: "Stopping", hint: "abort in flight" }
3932
+ };
3933
+ function goalStatusLabel(status) {
3934
+ return GOAL_STATUS_META[status] ?? { short: status, hint: "" };
3935
+ }
3936
+ function phaseLabel(phase) {
3937
+ return PHASE_META[phase] ?? { short: phase, hint: "" };
3938
+ }
3939
+ function describeGoalState(status, phase) {
3940
+ const goal = goalStatusLabel(status);
3941
+ const activity = phase ? phaseLabel(phase) : undefined;
3942
+ const workerClause = activity ? `worker is ${activity.hint || activity.short.toLowerCase()}` : "worker state unknown";
3943
+ if (status === "active")
3944
+ return `Loopd owns this; ${workerClause}.`;
3945
+ if (status === "complete")
3946
+ return "Done \u2014 verified; worker is stopped.";
3947
+ return `Parked \u2014 ${goal.hint || goal.short.toLowerCase()}; ${workerClause}.`;
3948
+ }
3949
+
3950
+ // src/server/owner-tools.ts
3775
3951
  import { promises as fs3 } from "fs";
3776
3952
  function withTimeout2(promise, ms) {
3777
3953
  return Promise.race([
@@ -3807,11 +3983,17 @@ function ownerTools(options) {
3807
3983
  }
3808
3984
  const summaries = goals.map((g) => {
3809
3985
  const runtime = state.runtimes.find((r) => r.goalID === g.id);
3986
+ const phase = runtime?.phase ?? "unknown";
3987
+ const goalLabel = goalStatusLabel(g.status);
3988
+ const activityLabel = phaseLabel(phase);
3810
3989
  return {
3811
3990
  id: g.id,
3812
3991
  name: g.name,
3813
3992
  status: g.status,
3814
- phase: runtime?.phase ?? "unknown",
3993
+ statusDisplay: `${goalLabel.short} \u2014 ${goalLabel.hint}`,
3994
+ phase,
3995
+ activityDisplay: `${activityLabel.short} \u2014 ${activityLabel.hint}`,
3996
+ stateSummary: describeGoalState(g.status, runtime?.phase),
3815
3997
  turn: runtime?.runCount ?? 0,
3816
3998
  budgetTurnCount: runtime?.budgetTurnCount ?? 0,
3817
3999
  maxTurns: g.config.maxTurns,
@@ -3881,6 +4063,9 @@ function ownerTools(options) {
3881
4063
  name: goal.name,
3882
4064
  objective: goal.objective,
3883
4065
  status: goal.status,
4066
+ statusDisplay: `${goalStatusLabel(goal.status).short} \u2014 ${goalStatusLabel(goal.status).hint}`,
4067
+ activityDisplay: runtime?.phase ? `${phaseLabel(runtime.phase).short} \u2014 ${phaseLabel(runtime.phase).hint}` : undefined,
4068
+ stateSummary: describeGoalState(goal.status, runtime?.phase),
3884
4069
  ownerSessionID: goal.ownerSessionID,
3885
4070
  workerSessionID: goal.workerSessionID,
3886
4071
  config: {
@@ -4337,6 +4522,9 @@ var PLUGIN_ID = "opencode-loopd.server";
4337
4522
  var server = async ({ client, directory }, pluginOptions) => {
4338
4523
  const defaults = parsePluginDefaults(pluginOptions);
4339
4524
  const host = createRealHost(client, directory);
4525
+ return createServerHooks(directory, host, defaults);
4526
+ };
4527
+ function createServerHooks(directory, host, defaults) {
4340
4528
  const goalService = createGoalService(host);
4341
4529
  const worker = createControlWorker({
4342
4530
  directory,
@@ -4465,7 +4653,7 @@ var server = async ({ client, directory }, pluginOptions) => {
4465
4653
  scheduleWorker.stop();
4466
4654
  }
4467
4655
  };
4468
- };
4656
+ }
4469
4657
  function parsePluginDefaults(options) {
4470
4658
  const agent = typeof options?.defaultAgent === "string" ? options.defaultAgent.trim() : "";
4471
4659
  const model = typeof options?.defaultModel === "string" ? options.defaultModel.trim() : "";
@@ -4476,10 +4664,127 @@ function parsePluginDefaults(options) {
4476
4664
  defaultChecks: checks.length > 0 ? checks : undefined
4477
4665
  };
4478
4666
  }
4479
- var v2 = define({
4667
+ var v2 = {
4480
4668
  id: PLUGIN_ID,
4481
- async setup() {}
4482
- });
4669
+ async setup(context) {
4670
+ const directory = context.location.directory;
4671
+ const statuses = new Map;
4672
+ const host = createV2Host(context, statuses);
4673
+ const hooks = createServerHooks(directory, host, parsePluginDefaults(context.options));
4674
+ const registrations = [];
4675
+ const eventController = new AbortController;
4676
+ let eventTask = Promise.resolve();
4677
+ let disposed = false;
4678
+ const cleanup = async () => {
4679
+ if (disposed)
4680
+ return;
4681
+ disposed = true;
4682
+ eventController.abort();
4683
+ await eventTask;
4684
+ try {
4685
+ await Promise.allSettled(registrations.reverse().map((registration) => registration.dispose()));
4686
+ } finally {
4687
+ await hooks.dispose?.();
4688
+ }
4689
+ };
4690
+ try {
4691
+ registrations.push(await context.tool.transform((editor) => {
4692
+ for (const [id, definition] of Object.entries(hooks.tool ?? {})) {
4693
+ editor.add(toV2Tool(id, definition, directory));
4694
+ }
4695
+ }));
4696
+ registrations.push(await context.tool.hook("execute.before", async (input) => {
4697
+ await hooks["tool.execute.before"]?.({
4698
+ tool: input.tool,
4699
+ sessionID: input.sessionID,
4700
+ callID: input.id
4701
+ }, { args: input.input });
4702
+ }));
4703
+ registrations.push(await context.tool.hook("execute.after", async (input) => {
4704
+ const output = input.status === "completed" ? input.result : { content: JSON.stringify(input.error) };
4705
+ await hooks["tool.execute.after"]?.({
4706
+ tool: input.tool,
4707
+ sessionID: input.sessionID,
4708
+ callID: input.id,
4709
+ args: input.input
4710
+ }, {
4711
+ title: "",
4712
+ output: typeof output.content === "string" ? output.content : JSON.stringify(output.content ?? ""),
4713
+ metadata: output.metadata ?? {}
4714
+ });
4715
+ }));
4716
+ eventTask = consumeV2Events(context, eventController.signal, statuses, hooks).catch(async (error) => {
4717
+ if (!eventController.signal.aborted) {
4718
+ await logServerEvent(directory, "events.failed", { detail: describeError(error) });
4719
+ }
4720
+ });
4721
+ return cleanup;
4722
+ } catch (error) {
4723
+ await cleanup();
4724
+ throw error;
4725
+ }
4726
+ }
4727
+ };
4728
+ function toV2Tool(id, definition, directory) {
4729
+ return {
4730
+ name: id,
4731
+ description: definition.description,
4732
+ input: v1Tool.schema.object(definition.args),
4733
+ async execute(input, context) {
4734
+ const result = await definition.execute(input, {
4735
+ sessionID: context.sessionID,
4736
+ agent: context.agent,
4737
+ messageID: context.messageID,
4738
+ directory,
4739
+ worktree: directory,
4740
+ abort: new AbortController().signal,
4741
+ metadata() {},
4742
+ async ask() {}
4743
+ });
4744
+ if (typeof result === "string")
4745
+ return { content: result };
4746
+ return {
4747
+ content: result.output,
4748
+ metadata: {
4749
+ ...result.metadata,
4750
+ ...result.title ? { title: result.title } : {}
4751
+ }
4752
+ };
4753
+ }
4754
+ };
4755
+ }
4756
+ async function consumeV2Events(context, signal, statuses, hooks) {
4757
+ for await (const event of context.event.subscribe({ signal })) {
4758
+ const data = "data" in event && event.data && typeof event.data === "object" ? event.data : {};
4759
+ const sessionID = typeof data.sessionID === "string" ? data.sessionID : undefined;
4760
+ if (sessionID) {
4761
+ if (event.type === "session.status")
4762
+ statuses.set(sessionID, data.status?.type ?? "unknown");
4763
+ else if (event.type === "session.idle")
4764
+ statuses.set(sessionID, "idle");
4765
+ else if (event.type === "session.execution.started")
4766
+ statuses.set(sessionID, "busy");
4767
+ }
4768
+ await hooks.event?.({ event: normalizeV2Event(event) });
4769
+ }
4770
+ }
4771
+ function normalizeV2Event(event) {
4772
+ const properties = event?.data && typeof event.data === "object" ? event.data : {};
4773
+ if (event?.type === "session.compaction.ended")
4774
+ return { type: "session.compacted", properties };
4775
+ if (event?.type === "session.execution.failed")
4776
+ return { type: "session.error", properties };
4777
+ if (event?.type === "session.message.content.updated") {
4778
+ return {
4779
+ type: "message.part.updated",
4780
+ properties: {
4781
+ sessionID: properties.sessionID,
4782
+ part: { messageID: properties.messageID }
4783
+ }
4784
+ };
4785
+ }
4786
+ return { type: event?.type, properties };
4787
+ }
4483
4788
  var plugin_default = {
4484
4789
  id: PLUGIN_ID,
4485
4790
  server,