@defend-tech/opencode-optima 0.1.69 → 0.1.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8530,6 +8530,7 @@ var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
8530
8530
  var CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS = 3e4;
8531
8531
  var CLICKUP_WEBHOOK_STARTUP_COMMENT_LOOKBACK_MS = 6 * 60 * 60 * 1e3;
8532
8532
  var CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS = 5 * 60 * 1e3;
8533
+ var CLICKUP_ASSIGNMENT_WATCHDOG_RUNNING_GRACE_MS = 15 * 60 * 1e3;
8533
8534
  var CLICKUP_WORKTREE_FAILURE_COMMENT_DEDUPE_MS = 10 * 60 * 1e3;
8534
8535
  var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
8535
8536
  var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
@@ -9816,6 +9817,10 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9816
9817
  assignmentWatchdogIntervalMs: normalizeNonNegativeInteger(
9817
9818
  opencode.assignment_watchdog_interval_ms ?? opencode.assignmentWatchdogIntervalMs ?? raw.assignment_watchdog_interval_ms ?? raw.assignmentWatchdogIntervalMs,
9818
9819
  CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS
9820
+ ),
9821
+ assignmentWatchdogRunningGraceMs: normalizeNonNegativeInteger(
9822
+ opencode.assignment_watchdog_running_grace_ms ?? opencode.assignmentWatchdogRunningGraceMs ?? raw.assignment_watchdog_running_grace_ms ?? raw.assignmentWatchdogRunningGraceMs,
9823
+ CLICKUP_ASSIGNMENT_WATCHDOG_RUNNING_GRACE_MS
9819
9824
  )
9820
9825
  },
9821
9826
  openchamber: {
@@ -10649,20 +10654,25 @@ function appendDirectoryQuery(url, directory) {
10649
10654
  const separator = url.includes("?") ? "&" : "?";
10650
10655
  return `${url}${separator}directory=${encodeURIComponent(value)}`;
10651
10656
  }
10652
- async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
10657
+ function normalizeOpenCodePromptDelivery(value, fallback = "queue") {
10658
+ const normalized = String(value || "").trim().toLowerCase();
10659
+ return normalized === "steer" || normalized === "queue" ? normalized : fallback;
10660
+ }
10661
+ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false, delivery = "queue" } = {}) {
10653
10662
  if (typeof fetchImpl !== "function") throw new Error("OpenCode direct prompt delivery requires fetch.");
10654
10663
  const root = normalizeOpenCodeBaseUrl(baseUrl, "");
10655
10664
  if (!root) throw new Error("OpenCode direct prompt delivery requires a base URL.");
10656
10665
  const encodedSession = encodeURIComponent(sessionId);
10666
+ const promptDelivery = normalizeOpenCodePromptDelivery(delivery, "queue");
10657
10667
  const attempts = [
10658
10668
  legacyOnly ? null : {
10659
10669
  name: "v2 prompt",
10660
10670
  url: appendDirectoryQuery(`${root}/api/session/${encodedSession}/prompt`, directory),
10661
- body: { prompt: { text }, delivery: "queue", resume: true },
10671
+ body: { prompt: { text }, delivery: promptDelivery, resume: true },
10662
10672
  accept: (response, data) => {
10663
10673
  if (!response.ok) return null;
10664
10674
  if (!openCodePromptAdmissionVerification(data, sessionId)) throw new Error("OpenCode v2 prompt response did not include a valid prompt admission.");
10665
- return { ok: true, method: "http", endpoint: "/api/session/{sessionID}/prompt", status: response.status, data: data.data, response: data };
10675
+ return { ok: true, method: "http", endpoint: "/api/session/{sessionID}/prompt", status: response.status, delivery: promptDelivery, data: data.data, response: data };
10666
10676
  }
10667
10677
  },
10668
10678
  {
@@ -10718,7 +10728,7 @@ async function callOpenCodePromptWithFallbacks(method, sessionId, flatPayload, s
10718
10728
  }
10719
10729
  throw firstError;
10720
10730
  }
10721
- async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false, allowDirectFallback = true } = {}) {
10731
+ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false, allowDirectFallback = true, directDelivery = "queue" } = {}) {
10722
10732
  const directBaseUrl = opencodeBaseUrl || baseUrl;
10723
10733
  const parts = [{ type: "text", text }];
10724
10734
  const flatPayload = { directory, agent, parts };
@@ -10742,7 +10752,7 @@ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, direct
10742
10752
  firstError ??= error;
10743
10753
  }
10744
10754
  }
10745
- if (allowDirectFallback && directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly });
10755
+ if (allowDirectFallback && directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly, delivery: directDelivery });
10746
10756
  if (firstError) throw firstError;
10747
10757
  throw new Error("OpenCode client does not expose session.prompt or session.promptAsync.");
10748
10758
  }
@@ -10771,6 +10781,22 @@ async function readOpenCodeSessionMessages(client, { sessionId, directory, limit
10771
10781
  if (directory) query.directory = directory;
10772
10782
  return normalizeOpenCodeSessionMessages(await client.session.messages({ path: { id: sessionId }, query }));
10773
10783
  }
10784
+ async function readOpenCodeChildSessions(client, { sessionId, directory } = {}) {
10785
+ if (typeof client?.session?.children !== "function") return [];
10786
+ const attempts = [
10787
+ { path: { id: sessionId }, query: directory ? { directory } : {} },
10788
+ { path: { sessionID: sessionId }, query: directory ? { directory } : {} },
10789
+ { sessionID: sessionId, ...directory ? { directory } : {} }
10790
+ ];
10791
+ for (const attempt of attempts) {
10792
+ try {
10793
+ const result = await client.session.children(attempt);
10794
+ return normalizeOpenCodeSessionCollection(result?.data ?? result);
10795
+ } catch {
10796
+ }
10797
+ }
10798
+ return [];
10799
+ }
10774
10800
  function openCodeResultSummary(result) {
10775
10801
  const data = result?.data ?? result;
10776
10802
  return {
@@ -10831,13 +10857,28 @@ function openCodeMessageTimestampMs(message = {}, key = "updated") {
10831
10857
  const number = Number(value);
10832
10858
  return Number.isFinite(number) && number > 0 ? number : 0;
10833
10859
  }
10834
- function isOpenCodeAssistantMessageRunning(message = {}) {
10860
+ function openCodeSessionTimestampMs(session = {}, key = "updated") {
10861
+ const time = session?.time || session?.info?.time || {};
10862
+ const value = key === "created" ? session.time_created ?? session.timeCreated ?? time.created : session.time_updated ?? session.timeUpdated ?? time.updated ?? session.time_created ?? session.timeCreated ?? time.created;
10863
+ const number = Number(value);
10864
+ return Number.isFinite(number) && number > 0 ? number : 0;
10865
+ }
10866
+ function openCodeNowMs(now = /* @__PURE__ */ new Date()) {
10867
+ const value = typeof now === "function" ? now() : now;
10868
+ const number = Number(value instanceof Date ? value.getTime() : new Date(value).getTime());
10869
+ return Number.isFinite(number) && number > 0 ? number : 0;
10870
+ }
10871
+ function isOpenCodeAssistantMessageRunning(message = {}, { latestActivityAt = 0, now = /* @__PURE__ */ new Date(), runningGraceMs = CLICKUP_ASSIGNMENT_WATCHDOG_RUNNING_GRACE_MS } = {}) {
10835
10872
  if (normalizeLooseToken(normalizeOpenCodeMessageRole(message)) !== "assistant") return false;
10836
10873
  const started = openCodeMessageTimestampMs(message, "created") > 0;
10837
10874
  if (!started) return false;
10838
- return openCodeMessageTimestampMs(message, "completed") === 0;
10875
+ if (openCodeMessageTimestampMs(message, "completed") !== 0) return false;
10876
+ const nowMs = openCodeNowMs(now);
10877
+ const activityAt = Number(latestActivityAt) || openCodeMessageTimestampMs(message, "updated") || openCodeMessageTimestampMs(message, "created");
10878
+ if (!Number.isFinite(nowMs) || nowMs <= 0 || !Number.isFinite(activityAt) || activityAt <= 0) return false;
10879
+ return nowMs - activityAt <= Math.max(0, Number(runningGraceMs) || 0);
10839
10880
  }
10840
- async function inspectOpenCodeSessionActivity(client, { sessionId, directory, limit = 10 } = {}) {
10881
+ async function inspectOpenCodeSessionActivity(client, { sessionId, directory, limit = 10, now = /* @__PURE__ */ new Date(), runningGraceMs = CLICKUP_ASSIGNMENT_WATCHDOG_RUNNING_GRACE_MS } = {}) {
10841
10882
  const messages = await readOpenCodeSessionMessages(client, { sessionId, directory, limit });
10842
10883
  if (!messages) return { ok: false, reason: "message_inspection_unavailable", sessionId, directory };
10843
10884
  const enriched = messages.map((message, index) => ({
@@ -10847,20 +10888,45 @@ async function inspectOpenCodeSessionActivity(client, { sessionId, directory, li
10847
10888
  createdAt: openCodeMessageTimestampMs(message, "created"),
10848
10889
  completedAt: openCodeMessageTimestampMs(message, "completed")
10849
10890
  })).sort((a, b) => a.sortTime - b.sortTime || a.index - b.index);
10891
+ const childSessions = await readOpenCodeChildSessions(client, { sessionId, directory });
10892
+ const childActivities = childSessions.map((session, index) => ({
10893
+ session,
10894
+ index,
10895
+ id: session.id || session.sessionID || session.sessionId || null,
10896
+ agent: session.agent || session.mode?.agent || session.metadata?.agent || "",
10897
+ updatedAt: openCodeSessionTimestampMs(session, "updated")
10898
+ })).filter((entry) => entry.updatedAt > 0).sort((a, b) => a.updatedAt - b.updatedAt || a.index - b.index);
10850
10899
  const assistantMessages = enriched.filter((entry) => normalizeLooseToken(normalizeOpenCodeMessageRole(entry.message)) === "assistant");
10851
10900
  const latestAssistant = assistantMessages.at(-1) || null;
10852
10901
  const latest = enriched.at(-1) || null;
10853
- const running = latestAssistant ? isOpenCodeAssistantMessageRunning(latestAssistant.message) : false;
10902
+ const latestChild = childActivities.at(-1) || null;
10903
+ const latestMessageActivityAt = latest?.sortTime || 0;
10904
+ const latestChildActivityAt = latestChild?.updatedAt || 0;
10905
+ const latestActivityAt = Math.max(latestMessageActivityAt, latestChildActivityAt);
10906
+ const nowMs = openCodeNowMs(now);
10907
+ const latestActivityAgeMs = Number.isFinite(nowMs) && latestActivityAt > 0 ? Math.max(0, nowMs - latestActivityAt) : null;
10908
+ const assistantRunning = latestAssistant ? isOpenCodeAssistantMessageRunning(latestAssistant.message, { latestActivityAt, now, runningGraceMs }) : false;
10909
+ const childRunning = latestChildActivityAt > 0 && latestActivityAgeMs !== null && latestActivityAgeMs <= Math.max(0, Number(runningGraceMs) || 0);
10910
+ const running = assistantRunning || childRunning;
10911
+ const runningReason = running ? childRunning && latestChildActivityAt >= latestMessageActivityAt ? "recent_child_session_activity" : "recent_incomplete_assistant" : latestAssistant && latestAssistant.completedAt === 0 ? "stale_incomplete_assistant" : "not_running";
10854
10912
  return {
10855
10913
  ok: true,
10856
10914
  sessionId,
10857
10915
  directory,
10858
10916
  count: messages.length,
10859
10917
  running,
10918
+ runningReason,
10919
+ runningGraceMs,
10920
+ latestActivityAt,
10921
+ latestActivityAgeMs,
10860
10922
  latestMessageId: latest ? normalizeOpenCodeMessageId(latest.message) : null,
10861
10923
  latestAssistantMessageId: latestAssistant ? normalizeOpenCodeMessageId(latestAssistant.message) : null,
10862
10924
  latestAssistantCreatedAt: latestAssistant?.createdAt || 0,
10863
- latestAssistantCompletedAt: latestAssistant?.completedAt || 0
10925
+ latestAssistantCompletedAt: latestAssistant?.completedAt || 0,
10926
+ latestChildSessionId: latestChild?.id || null,
10927
+ latestChildAgent: latestChild?.agent || null,
10928
+ latestChildUpdatedAt: latestChild?.updatedAt || 0,
10929
+ childSessionCount: childSessions.length
10864
10930
  };
10865
10931
  }
10866
10932
  function summarizeOpenCodeMessages(messages = [], { snippetLength = 160, maxMessages = 50 } = {}) {
@@ -11059,11 +11125,11 @@ function openCodeBlockingPromptVerification(result, sessionId) {
11059
11125
  if (parts.length > 0 || messageId) return { ok: true, method: parts.length > 0 ? "blocking_prompt_parts" : "blocking_prompt_message", messageId: messageId ? String(messageId) : null, sessionId: deliveredSessionId ? String(deliveredSessionId) : String(sessionId), parts: parts.length };
11060
11126
  return null;
11061
11127
  }
11062
- async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
11128
+ async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, directDelivery = "queue", acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
11063
11129
  const beforeMessages = await readOpenCodeSessionMessages(openCodeClient, { sessionId, directory, limit: 50 }).catch(() => null);
11064
11130
  let sendResult;
11065
11131
  try {
11066
- sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, allowDirectFallback: directPrompt });
11132
+ sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, directDelivery, allowDirectFallback: directPrompt });
11067
11133
  } catch (error) {
11068
11134
  const reason2 = error.message || "message_delivery_failed";
11069
11135
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason: reason2, fallbackAttempted: false });
@@ -11086,7 +11152,13 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
11086
11152
  }
11087
11153
  if (blockingPromptVerification) return { ok: true, verification: blockingPromptVerification, admissionVerification: null, fallback: false };
11088
11154
  if (admissionVerification && acceptPromptAdmission) {
11089
- return { ok: true, verification: admissionVerification, admissionVerification, fallback: false, admissionOnly: true };
11155
+ appendClickUpWebhookLocalLog(worktree, {
11156
+ type: "message_delivery_admission_not_sufficient",
11157
+ taskId,
11158
+ sessionId,
11159
+ admission: admissionVerification,
11160
+ policy: "clickup_routing_requires_visible_delivery"
11161
+ });
11090
11162
  }
11091
11163
  let verification = await verifySessionEventDelivery(openCodeClient, { sessionId, directory, beforeMessages, expectedText: text, markers: eventMarkers });
11092
11164
  if (verification?.ok) return { ok: true, verification, admissionVerification, fallback: false };
@@ -11489,10 +11561,9 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11489
11561
  const sessionId = String(existingSessionId);
11490
11562
  if (await sessionExists(openCodeClient, sessionId, { directory: taskRoute.worktree })) {
11491
11563
  if (typeof clickupClient?.updateTaskMetadata === "function") await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: metadataWithRouting });
11492
- const delivery = await deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl, directPrompt: config.opencode?.promptDelivery === "http", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery, applyBlockerOnFailure: false });
11564
+ const delivery = await deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl, directPrompt: config.opencode?.promptDelivery === "http", directDelivery: "steer", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery, applyBlockerOnFailure: false });
11493
11565
  if (!delivery.ok) {
11494
- const recovery2 = await recoverClickUpPmSession({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, staleSessionId: sessionId, sessionTitle, taskRoute, metadataWithRouting, config, prompt, eventMarkers: [taskId, eventType], deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, eventKey, createSession, verifySessionEventDelivery });
11495
- return finish(recovery2);
11566
+ return finish({ ...delivery, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, replacementAttempted: false });
11496
11567
  }
11497
11568
  return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryAdmission: delivery.admissionVerification, deliveryFallback: delivery.fallback, deliveryAttempts: delivery.fallback ? 2 : 1 });
11498
11569
  }
@@ -11620,7 +11691,12 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11620
11691
  const directory = String(getNestedMetadataValue(metadata, "task.worktree") || "").trim();
11621
11692
  if (sessionId) {
11622
11693
  try {
11623
- const activity = await inspectOpenCodeSessionActivity(openCodeClient, { sessionId, directory });
11694
+ const activity = await inspectOpenCodeSessionActivity(openCodeClient, {
11695
+ sessionId,
11696
+ directory,
11697
+ now,
11698
+ runningGraceMs: config.opencode.assignmentWatchdogRunningGraceMs
11699
+ });
11624
11700
  appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_session_activity", taskId, sessionId, directory: directory || null, ...activity });
11625
11701
  if (activity.running) {
11626
11702
  routed.ignored += 1;
@@ -8537,6 +8537,7 @@ var CLICKUP_WEBHOOK_STARTUP_COMMENT_LIMIT = 100;
8537
8537
  var CLICKUP_WEBHOOK_STARTUP_RECONCILIATION_DELAY_MS = 3e4;
8538
8538
  var CLICKUP_WEBHOOK_STARTUP_COMMENT_LOOKBACK_MS = 6 * 60 * 60 * 1e3;
8539
8539
  var CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS = 5 * 60 * 1e3;
8540
+ var CLICKUP_ASSIGNMENT_WATCHDOG_RUNNING_GRACE_MS = 15 * 60 * 1e3;
8540
8541
  var CLICKUP_WORKTREE_FAILURE_COMMENT_DEDUPE_MS = 10 * 60 * 1e3;
8541
8542
  var CLICKUP_WEBHOOK_LOG_LEVELS = /* @__PURE__ */ new Set(["error", "info", "verbose"]);
8542
8543
  var CLICKUP_WEBHOOK_REDACTED = "[REDACTED]";
@@ -9823,6 +9824,10 @@ function normalizeClickUpWebhookConfig(rawClickUp = null, worktree = process.cwd
9823
9824
  assignmentWatchdogIntervalMs: normalizeNonNegativeInteger(
9824
9825
  opencode.assignment_watchdog_interval_ms ?? opencode.assignmentWatchdogIntervalMs ?? raw.assignment_watchdog_interval_ms ?? raw.assignmentWatchdogIntervalMs,
9825
9826
  CLICKUP_ASSIGNMENT_WATCHDOG_INTERVAL_MS
9827
+ ),
9828
+ assignmentWatchdogRunningGraceMs: normalizeNonNegativeInteger(
9829
+ opencode.assignment_watchdog_running_grace_ms ?? opencode.assignmentWatchdogRunningGraceMs ?? raw.assignment_watchdog_running_grace_ms ?? raw.assignmentWatchdogRunningGraceMs,
9830
+ CLICKUP_ASSIGNMENT_WATCHDOG_RUNNING_GRACE_MS
9826
9831
  )
9827
9832
  },
9828
9833
  openchamber: {
@@ -10656,20 +10661,25 @@ function appendDirectoryQuery(url, directory) {
10656
10661
  const separator = url.includes("?") ? "&" : "?";
10657
10662
  return `${url}${separator}directory=${encodeURIComponent(value)}`;
10658
10663
  }
10659
- async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false } = {}) {
10664
+ function normalizeOpenCodePromptDelivery(value, fallback = "queue") {
10665
+ const normalized = String(value || "").trim().toLowerCase();
10666
+ return normalized === "steer" || normalized === "queue" ? normalized : fallback;
10667
+ }
10668
+ async function sendOpenCodeSessionEventDirect({ baseUrl, sessionId, text, agent, directory, fetchImpl = globalThis.fetch, legacyOnly = false, delivery = "queue" } = {}) {
10660
10669
  if (typeof fetchImpl !== "function") throw new Error("OpenCode direct prompt delivery requires fetch.");
10661
10670
  const root = normalizeOpenCodeBaseUrl(baseUrl, "");
10662
10671
  if (!root) throw new Error("OpenCode direct prompt delivery requires a base URL.");
10663
10672
  const encodedSession = encodeURIComponent(sessionId);
10673
+ const promptDelivery = normalizeOpenCodePromptDelivery(delivery, "queue");
10664
10674
  const attempts = [
10665
10675
  legacyOnly ? null : {
10666
10676
  name: "v2 prompt",
10667
10677
  url: appendDirectoryQuery(`${root}/api/session/${encodedSession}/prompt`, directory),
10668
- body: { prompt: { text }, delivery: "queue", resume: true },
10678
+ body: { prompt: { text }, delivery: promptDelivery, resume: true },
10669
10679
  accept: (response, data) => {
10670
10680
  if (!response.ok) return null;
10671
10681
  if (!openCodePromptAdmissionVerification(data, sessionId)) throw new Error("OpenCode v2 prompt response did not include a valid prompt admission.");
10672
- return { ok: true, method: "http", endpoint: "/api/session/{sessionID}/prompt", status: response.status, data: data.data, response: data };
10682
+ return { ok: true, method: "http", endpoint: "/api/session/{sessionID}/prompt", status: response.status, delivery: promptDelivery, data: data.data, response: data };
10673
10683
  }
10674
10684
  },
10675
10685
  {
@@ -10725,7 +10735,7 @@ async function callOpenCodePromptWithFallbacks(method, sessionId, flatPayload, s
10725
10735
  }
10726
10736
  throw firstError;
10727
10737
  }
10728
- async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false, allowDirectFallback = true } = {}) {
10738
+ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, directory, opencodeBaseUrl, baseUrl, fetchImpl, direct = false, legacyOnly = false, allowDirectFallback = true, directDelivery = "queue" } = {}) {
10729
10739
  const directBaseUrl = opencodeBaseUrl || baseUrl;
10730
10740
  const parts = [{ type: "text", text }];
10731
10741
  const flatPayload = { directory, agent, parts };
@@ -10749,7 +10759,7 @@ async function sendOpenCodeSessionEvent(client, { sessionId, agent, text, direct
10749
10759
  firstError ??= error;
10750
10760
  }
10751
10761
  }
10752
- if (allowDirectFallback && directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly });
10762
+ if (allowDirectFallback && directBaseUrl) return sendOpenCodeSessionEventDirect({ baseUrl: directBaseUrl, sessionId, text, agent, directory, fetchImpl, legacyOnly, delivery: directDelivery });
10753
10763
  if (firstError) throw firstError;
10754
10764
  throw new Error("OpenCode client does not expose session.prompt or session.promptAsync.");
10755
10765
  }
@@ -10778,6 +10788,22 @@ async function readOpenCodeSessionMessages(client, { sessionId, directory, limit
10778
10788
  if (directory) query.directory = directory;
10779
10789
  return normalizeOpenCodeSessionMessages(await client.session.messages({ path: { id: sessionId }, query }));
10780
10790
  }
10791
+ async function readOpenCodeChildSessions(client, { sessionId, directory } = {}) {
10792
+ if (typeof client?.session?.children !== "function") return [];
10793
+ const attempts = [
10794
+ { path: { id: sessionId }, query: directory ? { directory } : {} },
10795
+ { path: { sessionID: sessionId }, query: directory ? { directory } : {} },
10796
+ { sessionID: sessionId, ...directory ? { directory } : {} }
10797
+ ];
10798
+ for (const attempt of attempts) {
10799
+ try {
10800
+ const result = await client.session.children(attempt);
10801
+ return normalizeOpenCodeSessionCollection(result?.data ?? result);
10802
+ } catch {
10803
+ }
10804
+ }
10805
+ return [];
10806
+ }
10781
10807
  function openCodeResultSummary(result) {
10782
10808
  const data = result?.data ?? result;
10783
10809
  return {
@@ -10838,13 +10864,28 @@ function openCodeMessageTimestampMs(message = {}, key = "updated") {
10838
10864
  const number = Number(value);
10839
10865
  return Number.isFinite(number) && number > 0 ? number : 0;
10840
10866
  }
10841
- function isOpenCodeAssistantMessageRunning(message = {}) {
10867
+ function openCodeSessionTimestampMs(session = {}, key = "updated") {
10868
+ const time = session?.time || session?.info?.time || {};
10869
+ const value = key === "created" ? session.time_created ?? session.timeCreated ?? time.created : session.time_updated ?? session.timeUpdated ?? time.updated ?? session.time_created ?? session.timeCreated ?? time.created;
10870
+ const number = Number(value);
10871
+ return Number.isFinite(number) && number > 0 ? number : 0;
10872
+ }
10873
+ function openCodeNowMs(now = /* @__PURE__ */ new Date()) {
10874
+ const value = typeof now === "function" ? now() : now;
10875
+ const number = Number(value instanceof Date ? value.getTime() : new Date(value).getTime());
10876
+ return Number.isFinite(number) && number > 0 ? number : 0;
10877
+ }
10878
+ function isOpenCodeAssistantMessageRunning(message = {}, { latestActivityAt = 0, now = /* @__PURE__ */ new Date(), runningGraceMs = CLICKUP_ASSIGNMENT_WATCHDOG_RUNNING_GRACE_MS } = {}) {
10842
10879
  if (normalizeLooseToken(normalizeOpenCodeMessageRole(message)) !== "assistant") return false;
10843
10880
  const started = openCodeMessageTimestampMs(message, "created") > 0;
10844
10881
  if (!started) return false;
10845
- return openCodeMessageTimestampMs(message, "completed") === 0;
10882
+ if (openCodeMessageTimestampMs(message, "completed") !== 0) return false;
10883
+ const nowMs = openCodeNowMs(now);
10884
+ const activityAt = Number(latestActivityAt) || openCodeMessageTimestampMs(message, "updated") || openCodeMessageTimestampMs(message, "created");
10885
+ if (!Number.isFinite(nowMs) || nowMs <= 0 || !Number.isFinite(activityAt) || activityAt <= 0) return false;
10886
+ return nowMs - activityAt <= Math.max(0, Number(runningGraceMs) || 0);
10846
10887
  }
10847
- async function inspectOpenCodeSessionActivity(client, { sessionId, directory, limit = 10 } = {}) {
10888
+ async function inspectOpenCodeSessionActivity(client, { sessionId, directory, limit = 10, now = /* @__PURE__ */ new Date(), runningGraceMs = CLICKUP_ASSIGNMENT_WATCHDOG_RUNNING_GRACE_MS } = {}) {
10848
10889
  const messages = await readOpenCodeSessionMessages(client, { sessionId, directory, limit });
10849
10890
  if (!messages) return { ok: false, reason: "message_inspection_unavailable", sessionId, directory };
10850
10891
  const enriched = messages.map((message, index) => ({
@@ -10854,20 +10895,45 @@ async function inspectOpenCodeSessionActivity(client, { sessionId, directory, li
10854
10895
  createdAt: openCodeMessageTimestampMs(message, "created"),
10855
10896
  completedAt: openCodeMessageTimestampMs(message, "completed")
10856
10897
  })).sort((a, b) => a.sortTime - b.sortTime || a.index - b.index);
10898
+ const childSessions = await readOpenCodeChildSessions(client, { sessionId, directory });
10899
+ const childActivities = childSessions.map((session, index) => ({
10900
+ session,
10901
+ index,
10902
+ id: session.id || session.sessionID || session.sessionId || null,
10903
+ agent: session.agent || session.mode?.agent || session.metadata?.agent || "",
10904
+ updatedAt: openCodeSessionTimestampMs(session, "updated")
10905
+ })).filter((entry) => entry.updatedAt > 0).sort((a, b) => a.updatedAt - b.updatedAt || a.index - b.index);
10857
10906
  const assistantMessages = enriched.filter((entry) => normalizeLooseToken(normalizeOpenCodeMessageRole(entry.message)) === "assistant");
10858
10907
  const latestAssistant = assistantMessages.at(-1) || null;
10859
10908
  const latest = enriched.at(-1) || null;
10860
- const running = latestAssistant ? isOpenCodeAssistantMessageRunning(latestAssistant.message) : false;
10909
+ const latestChild = childActivities.at(-1) || null;
10910
+ const latestMessageActivityAt = latest?.sortTime || 0;
10911
+ const latestChildActivityAt = latestChild?.updatedAt || 0;
10912
+ const latestActivityAt = Math.max(latestMessageActivityAt, latestChildActivityAt);
10913
+ const nowMs = openCodeNowMs(now);
10914
+ const latestActivityAgeMs = Number.isFinite(nowMs) && latestActivityAt > 0 ? Math.max(0, nowMs - latestActivityAt) : null;
10915
+ const assistantRunning = latestAssistant ? isOpenCodeAssistantMessageRunning(latestAssistant.message, { latestActivityAt, now, runningGraceMs }) : false;
10916
+ const childRunning = latestChildActivityAt > 0 && latestActivityAgeMs !== null && latestActivityAgeMs <= Math.max(0, Number(runningGraceMs) || 0);
10917
+ const running = assistantRunning || childRunning;
10918
+ const runningReason = running ? childRunning && latestChildActivityAt >= latestMessageActivityAt ? "recent_child_session_activity" : "recent_incomplete_assistant" : latestAssistant && latestAssistant.completedAt === 0 ? "stale_incomplete_assistant" : "not_running";
10861
10919
  return {
10862
10920
  ok: true,
10863
10921
  sessionId,
10864
10922
  directory,
10865
10923
  count: messages.length,
10866
10924
  running,
10925
+ runningReason,
10926
+ runningGraceMs,
10927
+ latestActivityAt,
10928
+ latestActivityAgeMs,
10867
10929
  latestMessageId: latest ? normalizeOpenCodeMessageId(latest.message) : null,
10868
10930
  latestAssistantMessageId: latestAssistant ? normalizeOpenCodeMessageId(latestAssistant.message) : null,
10869
10931
  latestAssistantCreatedAt: latestAssistant?.createdAt || 0,
10870
- latestAssistantCompletedAt: latestAssistant?.completedAt || 0
10932
+ latestAssistantCompletedAt: latestAssistant?.completedAt || 0,
10933
+ latestChildSessionId: latestChild?.id || null,
10934
+ latestChildAgent: latestChild?.agent || null,
10935
+ latestChildUpdatedAt: latestChild?.updatedAt || 0,
10936
+ childSessionCount: childSessions.length
10871
10937
  };
10872
10938
  }
10873
10939
  function summarizeOpenCodeMessages(messages = [], { snippetLength = 160, maxMessages = 50 } = {}) {
@@ -11066,11 +11132,11 @@ function openCodeBlockingPromptVerification(result, sessionId) {
11066
11132
  if (parts.length > 0 || messageId) return { ok: true, method: parts.length > 0 ? "blocking_prompt_parts" : "blocking_prompt_message", messageId: messageId ? String(messageId) : null, sessionId: deliveredSessionId ? String(deliveredSessionId) : String(sessionId), parts: parts.length };
11067
11133
  return null;
11068
11134
  }
11069
- async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
11135
+ async function deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent, text, directory, opencodeBaseUrl, directPrompt = false, directDelivery = "queue", acceptPromptAdmission = false, eventMarkers = [], verifySessionEventDelivery = verifyOpenCodeSessionEventDelivery, applyBlockerOnFailure = true } = {}) {
11070
11136
  const beforeMessages = await readOpenCodeSessionMessages(openCodeClient, { sessionId, directory, limit: 50 }).catch(() => null);
11071
11137
  let sendResult;
11072
11138
  try {
11073
- sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, allowDirectFallback: directPrompt });
11139
+ sendResult = await sendSessionEvent(openCodeClient, { sessionId, agent, text, directory, opencodeBaseUrl, direct: directPrompt, directDelivery, allowDirectFallback: directPrompt });
11074
11140
  } catch (error) {
11075
11141
  const reason2 = error.message || "message_delivery_failed";
11076
11142
  appendClickUpWebhookLocalLog(worktree, { type: "message_delivery_failed", taskId, sessionId, reason: reason2, fallbackAttempted: false });
@@ -11093,7 +11159,13 @@ async function deliverClickUpSessionEventWithVerification({ openCodeClient, send
11093
11159
  }
11094
11160
  if (blockingPromptVerification) return { ok: true, verification: blockingPromptVerification, admissionVerification: null, fallback: false };
11095
11161
  if (admissionVerification && acceptPromptAdmission) {
11096
- return { ok: true, verification: admissionVerification, admissionVerification, fallback: false, admissionOnly: true };
11162
+ appendClickUpWebhookLocalLog(worktree, {
11163
+ type: "message_delivery_admission_not_sufficient",
11164
+ taskId,
11165
+ sessionId,
11166
+ admission: admissionVerification,
11167
+ policy: "clickup_routing_requires_visible_delivery"
11168
+ });
11097
11169
  }
11098
11170
  let verification = await verifySessionEventDelivery(openCodeClient, { sessionId, directory, beforeMessages, expectedText: text, markers: eventMarkers });
11099
11171
  if (verification?.ok) return { ok: true, verification, admissionVerification, fallback: false };
@@ -11496,10 +11568,9 @@ async function routeClickUpWebhookEventUnlocked({ payload, config, state = {}, w
11496
11568
  const sessionId = String(existingSessionId);
11497
11569
  if (await sessionExists(openCodeClient, sessionId, { directory: taskRoute.worktree })) {
11498
11570
  if (typeof clickupClient?.updateTaskMetadata === "function") await clickupClient.updateTaskMetadata({ taskId, fieldId: config.routing.metadataFieldId, value: metadataWithRouting });
11499
- const delivery = await deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl, directPrompt: config.opencode?.promptDelivery === "http", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery, applyBlockerOnFailure: false });
11571
+ const delivery = await deliverClickUpSessionEventWithVerification({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, sessionId, agent: config.routing.targetAgent, text: prompt, directory: taskRoute.worktree, opencodeBaseUrl: config.opencode?.baseUrl, directPrompt: config.opencode?.promptDelivery === "http", directDelivery: "steer", acceptPromptAdmission: config.opencode?.acceptPromptAdmission === true, eventMarkers: [taskId, eventType], verifySessionEventDelivery, applyBlockerOnFailure: false });
11500
11572
  if (!delivery.ok) {
11501
- const recovery2 = await recoverClickUpPmSession({ openCodeClient, sendSessionEvent, clickupClient, worktree, taskId, staleSessionId: sessionId, sessionTitle, taskRoute, metadataWithRouting, config, prompt, eventMarkers: [taskId, eventType], deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, eventKey, createSession, verifySessionEventDelivery });
11502
- return finish(recovery2);
11573
+ return finish({ ...delivery, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, replacementAttempted: false });
11503
11574
  }
11504
11575
  return finish({ ok: true, action: "sent_to_existing_session", taskId, sessionId, eventKey, branch: taskRoute.branch, worktree: taskRoute.worktree, deliveryEvidencePath, evidencePath: routingMetadata.evidence_path, deliveryVerification: delivery.verification, deliveryAdmission: delivery.admissionVerification, deliveryFallback: delivery.fallback, deliveryAttempts: delivery.fallback ? 2 : 1 });
11505
11576
  }
@@ -11627,7 +11698,12 @@ async function reconcileClickUpStartup({ config, state = {}, worktree = process.
11627
11698
  const directory = String(getNestedMetadataValue(metadata, "task.worktree") || "").trim();
11628
11699
  if (sessionId) {
11629
11700
  try {
11630
- const activity = await inspectOpenCodeSessionActivity(openCodeClient, { sessionId, directory });
11701
+ const activity = await inspectOpenCodeSessionActivity(openCodeClient, {
11702
+ sessionId,
11703
+ directory,
11704
+ now,
11705
+ runningGraceMs: config.opencode.assignmentWatchdogRunningGraceMs
11706
+ });
11631
11707
  appendClickUpWebhookLocalLog(worktree, { type: "assignment_watchdog_session_activity", taskId, sessionId, directory: directory || null, ...activity });
11632
11708
  if (activity.running) {
11633
11709
  routed.ignored += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defend-tech/opencode-optima",
3
- "version": "0.1.69",
3
+ "version": "0.1.71",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+ssh://git@github.com/defend-tech/opencode-optima.git"