opencode-goal-plugin 0.6.8 → 0.7.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.
@@ -95,6 +95,7 @@ function createRuntimeState() {
95
95
  sessionArchive: new Map(),
96
96
  sessionOrdered: new Set(),
97
97
  lastGoalResults: new Map(),
98
+ sessionMutationVersions: new Map(),
98
99
  seenTokens: new Map(),
99
100
  seenUsage: new Map(),
100
101
  seenOutputTokens: new Map(),
@@ -165,6 +166,7 @@ const sessionArchive = runtimeCollection("sessionArchive")
165
166
  const sessionOrdered = runtimeCollection("sessionOrdered")
166
167
  const MAX_ARCHIVED_PER_SESSION = 10
167
168
  const lastGoalResults = runtimeCollection("lastGoalResults")
169
+ const sessionMutationVersions = runtimeCollection("sessionMutationVersions")
168
170
  const seenTokens = runtimeCollection("seenTokens")
169
171
  const seenUsage = runtimeCollection("seenUsage")
170
172
  const seenOutputTokens = runtimeCollection("seenOutputTokens")
@@ -490,6 +492,7 @@ function emitLedgerEvent(goal, type, detail, timestamp) {
490
492
  options: goal.options,
491
493
  stopped: goal.stopped,
492
494
  stopReason: goal.stopReason,
495
+ blockedReason: goal.blockedReason,
493
496
  ordered: sessionOrdered.has(goal.sessionID),
494
497
  },
495
498
  type,
@@ -504,6 +507,7 @@ function emitLedgerEvent(goal, type, detail, timestamp) {
504
507
  function pushHistory(goal, type, detail, timestamp = Date.now()) {
505
508
  const entry = makeHistoryEntry(type, detail, timestamp)
506
509
  goal.history = [...(goal.history || []), entry].slice(-MAX_HISTORY_ENTRIES)
510
+ markSessionMutation(goal.sessionID)
507
511
  return emitLedgerEvent(goal, entry.type, entry.detail, entry.timestamp)
508
512
  }
509
513
 
@@ -638,6 +642,7 @@ function reconstructGoalsFromLedger(entries) {
638
642
  const condition = [...events].reverse().find((event) => typeof event.condition === "string" && event.condition.trim())?.condition?.trim()
639
643
  if (!condition) continue
640
644
  const snapshot = [...events].reverse().find((event) => isPlainObject(event.snapshot))?.snapshot || {}
645
+ const latestBlocked = [...events].reverse().find((event) => event.type === "blocked")
641
646
 
642
647
  const history = events
643
648
  .map((event) =>
@@ -659,6 +664,12 @@ function reconstructGoalsFromLedger(entries) {
659
664
  options: isPlainObject(snapshot.options) ? snapshot.options : {},
660
665
  stopped: snapshot.stopped === true,
661
666
  stopReason: typeof snapshot.stopReason === "string" ? snapshot.stopReason : "",
667
+ blockedReason:
668
+ typeof snapshot.blockedReason === "string"
669
+ ? snapshot.blockedReason
670
+ : snapshot.stopReason === "blocked" && typeof latestBlocked?.detail === "string"
671
+ ? latestBlocked.detail
672
+ : "",
662
673
  ordered: snapshot.ordered === true || events.some((event) => /ordered goal/i.test(String(event.detail || ""))),
663
674
  startedAt: normalizeTimestamp(events[0]?.ts),
664
675
  history,
@@ -675,9 +686,19 @@ function recordCheckpoint(goal, text, timestamp = Date.now()) {
675
686
  const checkpoint = { summary, timestamp }
676
687
  goal.lastCheckpoint = checkpoint
677
688
  goal.checkpoints = [...(goal.checkpoints || []), checkpoint].slice(-MAX_CHECKPOINTS)
689
+ markSessionMutation(goal.sessionID)
678
690
  }
679
691
 
680
- function formatStatus(goal, commandName = "goal") {
692
+ function goalDisplayState(goal) {
693
+ if (!goal?.stopped) return "active"
694
+ return goal.stopReason === "blocked" ? "blocked" : "paused"
695
+ }
696
+
697
+ function formatStatus(
698
+ goal,
699
+ commandName = "goal",
700
+ completionAuditLabel = "evidence gate only (independent verifier off)",
701
+ ) {
681
702
  const elapsed = Math.round((Date.now() - goal.startedAt) / 1000)
682
703
  const lastProgress =
683
704
  goal.lastProgressAt > 0
@@ -688,6 +709,8 @@ function formatStatus(goal, commandName = "goal") {
688
709
  : "none yet"
689
710
  const lines = [
690
711
  `Active goal: ${goal.condition}`,
712
+ `State: ${goalDisplayState(goal)}`,
713
+ `Completion audit: ${completionAuditLabel}`,
691
714
  ]
692
715
  if (goal.successCriteria) lines.push(`Success criteria: ${goal.successCriteria}`)
693
716
  if (goal.constraints) lines.push(`Constraints: ${goal.constraints}`)
@@ -772,8 +795,16 @@ function sessionGoalMap(sessionID) {
772
795
  return map
773
796
  }
774
797
 
798
+ function markSessionMutation(sessionID) {
799
+ if (!sessionID) return 0
800
+ const next = (sessionMutationVersions.get(sessionID) || 0) + 1
801
+ sessionMutationVersions.set(sessionID, next)
802
+ return next
803
+ }
804
+
775
805
  function registerSessionGoal(goal) {
776
806
  sessionGoalMap(goal.sessionID).set(goal.goalId, goal)
807
+ markSessionMutation(goal.sessionID)
777
808
  }
778
809
 
779
810
  function listSessionGoals(sessionID) {
@@ -796,12 +827,13 @@ function setBoundedMessageValue(map, messageID, value) {
796
827
  function removeSessionGoal(sessionID, goalId) {
797
828
  const map = sessionGoals.get(sessionID)
798
829
  if (!map) return
799
- map.delete(goalId)
830
+ if (map.delete(goalId)) markSessionMutation(sessionID)
800
831
  if (map.size === 0) sessionGoals.delete(sessionID)
801
832
  }
802
833
 
803
834
  function focusGoal(sessionID, goal) {
804
835
  goalStates.set(sessionID, goal)
836
+ markSessionMutation(sessionID)
805
837
  }
806
838
 
807
839
  function pauseGoalClock(goal, timestamp = Date.now()) {
@@ -858,6 +890,10 @@ function cleanupGoal(sessionID) {
858
890
  }
859
891
  goalStates.delete(sessionID)
860
892
  activeContinues.delete(sessionID)
893
+ // Increment even when no focused goal remains. A concurrent clear of a
894
+ // provisional completion is otherwise indistinguishable from unrelated
895
+ // global result-retention pruning while its terminal write is in flight.
896
+ markSessionMutation(sessionID)
861
897
  }
862
898
 
863
899
  function clearRuntimeState() {
@@ -868,6 +904,7 @@ function clearRuntimeState() {
868
904
  sessionArchive.clear()
869
905
  sessionOrdered.clear()
870
906
  lastGoalResults.clear()
907
+ sessionMutationVersions.clear()
871
908
  seenTokens.clear()
872
909
  seenUsage.clear()
873
910
  seenOutputTokens.clear()
@@ -908,6 +945,7 @@ function clearSessionRuntimeState(
908
945
  runtime.sessionStatuses.delete(sessionID)
909
946
  if (!preserveExecutionContext) runtime.sessionExecutionContexts.delete(sessionID)
910
947
  runtime.passiveSessions.delete(sessionID)
948
+ markSessionMutation(sessionID)
911
949
  if (!preserveCommandSecurity) {
912
950
  runtime.pendingCommandTurns.delete(sessionID)
913
951
  runtime.activeCommandTurns.delete(sessionID)
@@ -966,16 +1004,60 @@ function rememberGoalResult(sessionID, goal, state, reason = "", evidence = "")
966
1004
  lastGoalResults.delete(sessionID)
967
1005
  lastGoalResults.set(sessionID, result)
968
1006
  // Keep a per-session archive so completed goals stay readable via /goal list.
969
- archiveSessionResult(sessionID, { ...result })
1007
+ const archivedResult = { ...result }
1008
+ archiveSessionResult(sessionID, archivedResult)
970
1009
  pruneGoalResults(goal.options)
1010
+ markSessionMutation(sessionID)
1011
+ return { lastResult: result, archivedResult }
971
1012
  }
972
1013
 
973
- function restoreAfterTerminalPersistenceFailure(sessionID, goal, { ordered = false } = {}) {
974
- lastGoalResults.delete(sessionID)
1014
+ function captureFocusedGoalSnapshot(sessionID) {
1015
+ const goal = goalStates.get(sessionID) || null
1016
+ return {
1017
+ goal,
1018
+ serialized: goal ? JSON.stringify(serializeGoal(goal)) : "",
1019
+ mutationVersion: sessionMutationVersions.get(sessionID) || 0,
1020
+ }
1021
+ }
1022
+
1023
+ function focusedGoalSnapshotIsCurrent(sessionID, snapshot) {
1024
+ const current = goalStates.get(sessionID) || null
1025
+ if (current !== snapshot?.goal) return false
1026
+ if ((sessionMutationVersions.get(sessionID) || 0) !== snapshot?.mutationVersion) return false
1027
+ return !current || JSON.stringify(serializeGoal(current)) === snapshot.serialized
1028
+ }
1029
+
1030
+ function restoreAfterTerminalPersistenceFailure(
1031
+ sessionID,
1032
+ goal,
1033
+ { ordered = false, expectedCurrentSnapshot, expectedResult } = {},
1034
+ ) {
1035
+ // A terminal write can yield while another command replaces, edits, pauses,
1036
+ // resumes, clears, or advances the session. Never roll the old goal back over
1037
+ // that newer state. The per-session mutation version catches a concurrent
1038
+ // clear even when both the expected and current focused goal are null, while
1039
+ // remaining unaffected by result-retention pruning in a different session.
1040
+ const expectedLastResult = expectedResult?.lastResult || expectedResult
1041
+ const expectedArchivedResult = expectedResult?.archivedResult
1042
+ const canRestore =
1043
+ !expectedCurrentSnapshot ||
1044
+ focusedGoalSnapshotIsCurrent(sessionID, expectedCurrentSnapshot)
1045
+
1046
+ // Remove only this failed provisional completion record. A newer concurrent
1047
+ // result/archive entry belongs to the newer operation and must survive.
1048
+ if (expectedLastResult && lastGoalResults.get(sessionID) === expectedLastResult) {
1049
+ lastGoalResults.delete(sessionID)
1050
+ }
975
1051
  const archived = sessionArchive.get(sessionID) || []
976
- if (archived.length) {
1052
+ if (expectedArchivedResult) {
1053
+ const retained = archived.filter((entry) => entry !== expectedArchivedResult)
1054
+ if (retained.length) sessionArchive.set(sessionID, retained)
1055
+ else sessionArchive.delete(sessionID)
1056
+ } else if (archived.length) {
977
1057
  sessionArchive.set(sessionID, archived.slice(0, -1))
978
1058
  }
1059
+
1060
+ if (!canRestore) return false
979
1061
  const prematurelyPromoted = goalStates.get(sessionID)
980
1062
  if (prematurelyPromoted && prematurelyPromoted.goalId !== goal.goalId) {
981
1063
  prematurelyPromoted.stopped = true
@@ -990,6 +1072,7 @@ function restoreAfterTerminalPersistenceFailure(sessionID, goal, { ordered = fal
990
1072
  goal.lastStatus = "Terminal state could not be persisted. Goal kept paused; fix storage and retry."
991
1073
  registerSessionGoal(goal)
992
1074
  focusGoal(sessionID, goal)
1075
+ return true
993
1076
  }
994
1077
 
995
1078
  function resetGoalBudget(goal) {
@@ -1543,15 +1626,15 @@ async function applyParsedStateFile(raw, client, onlySessionID = null) {
1543
1626
  }
1544
1627
 
1545
1628
  // After applyParsedStateFile loads goals into goalStates, check the ledger for
1546
- // terminal events. If a goal has a "completed" or "cleared" entry in the ledger
1547
- // but still appears active in the state file (because the state write failed
1548
- // after the terminal ledger write), remove it so it is not re-driven.
1629
+ // state transitions that landed after the snapshot. Completed/cleared goals are
1630
+ // removed so they cannot be re-driven, while a newer blocked event is overlaid
1631
+ // so its state and concrete reason survive a failed snapshot write.
1549
1632
  async function reconcileLoadedStateWithLedger(persistenceOptions, client, onlySessionID = null) {
1550
1633
  const entries = await readLedgerEntries(persistenceOptions.ledgerFilePath, {
1551
1634
  maxBytes: persistenceOptions.ledgerMaxBytes,
1552
1635
  retentionFiles: persistenceOptions.ledgerRetentionFiles,
1553
1636
  })
1554
- if (!entries.length) return
1637
+ if (!entries.length) return { removed: 0, blocked: 0 }
1555
1638
 
1556
1639
  const terminalGoals = new Set()
1557
1640
  for (const entry of entries) {
@@ -1564,16 +1647,69 @@ async function reconcileLoadedStateWithLedger(persistenceOptions, client, onlySe
1564
1647
  terminalGoals.add(`${entry.sessionID}\0${entry.goalId}`)
1565
1648
  }
1566
1649
  }
1567
- if (!terminalGoals.size) return
1568
-
1569
1650
  let removed = 0
1651
+ let blocked = 0
1570
1652
  for (const [sessionID, goals] of sessionGoals.entries()) {
1571
1653
  if (onlySessionID && sessionID !== onlySessionID) continue
1572
1654
  for (const goal of [...goals.values()]) {
1573
- if (!terminalGoals.has(`${sessionID}\0${goal.goalId}`)) continue
1574
- removeSessionGoal(sessionID, goal.goalId)
1575
- if (goalStates.get(sessionID)?.goalId === goal.goalId) goalStates.delete(sessionID)
1576
- removed += 1
1655
+ const key = `${sessionID}\0${goal.goalId}`
1656
+ if (terminalGoals.has(key)) {
1657
+ removeSessionGoal(sessionID, goal.goalId)
1658
+ if (goalStates.get(sessionID)?.goalId === goal.goalId) goalStates.delete(sessionID)
1659
+ removed += 1
1660
+ continue
1661
+ }
1662
+
1663
+ const persistedHistory = (goal.history || []).filter((event) => event.type !== "recovered")
1664
+ const latestPersistedTimestamp = persistedHistory.reduce(
1665
+ (latest, event) => Math.max(latest, normalizeTimestamp(event.timestamp, 0)),
1666
+ 0,
1667
+ )
1668
+ let latestLedgerState = null
1669
+ let latestLedgerTimestamp = -1
1670
+ for (const entry of entries) {
1671
+ if (entry.sessionID !== sessionID || entry.goalId !== goal.goalId || entry.type === "recovered") continue
1672
+ const timestamp = normalizeTimestamp(entry.ts, 0)
1673
+ if (timestamp < latestPersistedTimestamp) continue
1674
+ const detail = summarizeText(entry.detail, 400)
1675
+ const alreadyApplied = persistedHistory.some(
1676
+ (event) =>
1677
+ event.type === entry.type &&
1678
+ normalizeTimestamp(event.timestamp, 0) === timestamp &&
1679
+ event.detail === detail,
1680
+ )
1681
+ if (timestamp >= latestLedgerTimestamp) {
1682
+ latestLedgerState = { entry, alreadyApplied }
1683
+ latestLedgerTimestamp = timestamp
1684
+ }
1685
+ }
1686
+ if (
1687
+ latestLedgerState?.alreadyApplied ||
1688
+ latestLedgerState?.entry?.type !== "blocked" ||
1689
+ latestLedgerState.entry.snapshot?.stopped !== true ||
1690
+ latestLedgerState.entry.snapshot?.stopReason !== "blocked"
1691
+ ) continue
1692
+
1693
+ const reason = summarizeText(
1694
+ latestLedgerState.entry.snapshot?.blockedReason || latestLedgerState.entry.detail,
1695
+ MAX_GOAL_BLOCKER_LENGTH,
1696
+ )
1697
+ if (!reason) continue
1698
+ goal.stopped = true
1699
+ goal.stopReason = "blocked"
1700
+ goal.blockedReason = reason
1701
+ goal.lastStatus = "Recovered blocked goal state from the lifecycle ledger after the saved snapshot lagged behind."
1702
+ goal.continuationClaim = null
1703
+ goal.history = [
1704
+ ...(goal.history || []),
1705
+ makeHistoryEntry(
1706
+ "blocked",
1707
+ reason,
1708
+ normalizeTimestamp(latestLedgerState.entry.ts),
1709
+ ),
1710
+ ].slice(-MAX_HISTORY_ENTRIES)
1711
+ pauseGoalClock(goal)
1712
+ blocked += 1
1577
1713
  }
1578
1714
  if (!goalStates.has(sessionID) && sessionOrdered.has(sessionID) && goals.size > 0) {
1579
1715
  promoteNextOrderedGoal(sessionID)
@@ -1585,6 +1721,13 @@ async function reconcileLoadedStateWithLedger(persistenceOptions, client, onlySe
1585
1721
  `Ledger cross-check: removed ${removed} goal(s) whose terminal state was recorded in the ledger but not yet reflected in the state file (likely a failed terminal persist).`,
1586
1722
  )
1587
1723
  }
1724
+ if (blocked > 0) {
1725
+ await logPluginError(
1726
+ client,
1727
+ `Ledger cross-check: restored ${blocked} blocked goal(s) whose blocked state was recorded in the ledger but not yet reflected in the state file (likely a failed terminal persist).`,
1728
+ )
1729
+ }
1730
+ return { removed, blocked }
1588
1731
  }
1589
1732
 
1590
1733
  async function pathExists(path) {
@@ -1810,8 +1953,8 @@ async function loadPersistedSessionState(persistence, client, sessionID) {
1810
1953
  const state = await readPersistedStateFile(persistence.stateFilePath, client)
1811
1954
  if (state.status === "loaded") {
1812
1955
  await applyParsedStateFile(state.raw, client, sessionID)
1813
- await reconcileLoadedStateWithLedger(persistence, client, sessionID)
1814
- return "loaded"
1956
+ const reconciliation = await reconcileLoadedStateWithLedger(persistence, client, sessionID)
1957
+ return reconciliation.blocked > 0 ? "reconciled-blocked" : "loaded"
1815
1958
  }
1816
1959
  const recovered = await reconstructFromLedger(persistence, client, sessionID)
1817
1960
  if (state.status === "invalid" && recovered === "reconstructed") {
@@ -2802,6 +2945,8 @@ function buildGoalState(sessionID, condition, options, meta = {}, lastStatus = "
2802
2945
  }
2803
2946
 
2804
2947
  const AGENT_UPDATE_STATUSES = new Set(["complete", "blocked", "paused", "resumed"])
2948
+ const AGENT_COMPLETE_SUCCESS = "Goal marked complete and archived."
2949
+ const AGENT_BLOCK_SUCCESS = "Goal marked blocked."
2805
2950
 
2806
2951
  // Programmatic equivalents of the /goal command, exposed to the agent as tools
2807
2952
  // Each handler operates on a session id and mutates
@@ -2810,14 +2955,24 @@ const AGENT_UPDATE_STATUSES = new Set(["complete", "blocked", "paused", "resumed
2810
2955
  // result. Goal creation/replacement routes through the multi-goal registry
2811
2956
  // (buildGoalState + registerSessionGoal + focusGoal) exactly like the command
2812
2957
  // path, so tool-created goals persist and are driven by the idle handler.
2813
- function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalState = null, completionAuditor = null, commandName = "goal" }) {
2958
+ function buildAgentToolHandlers({
2959
+ defaultGoalOptions,
2960
+ persist,
2961
+ persistTerminalState = null,
2962
+ completionAuditor = null,
2963
+ completionAuditLabel = "evidence gate only (independent verifier off)",
2964
+ announceAudit = async () => {},
2965
+ auditMessagesEnabled = false,
2966
+ announceLifecycle = () => {},
2967
+ commandName = "goal",
2968
+ }) {
2814
2969
  // Use persistTerminalState (which logs on failure) for terminal operations when
2815
2970
  // available; fall back to plain persist for callers that don't wire it up (e.g.
2816
2971
  // tests using buildAgentToolHandlers directly).
2817
2972
  const persistFinal = persistTerminalState || persist
2818
2973
  async function getGoal(sessionID) {
2819
2974
  const goal = goalStates.get(sessionID)
2820
- if (goal) return formatStatus(goal)
2975
+ if (goal) return formatStatus(goal, commandName, completionAuditLabel)
2821
2976
  const lastResult = lastGoalResults.get(sessionID)
2822
2977
  if (lastResult) return formatGoalResult(lastResult)
2823
2978
  return "No active goal."
@@ -2887,12 +3042,18 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
2887
3042
  // Mirror the `/goal <condition>` replace path: discard the focused goal and
2888
3043
  // its saved result, drop any ordered sequence, then register + focus the new
2889
3044
  // goal so it persists and the idle handler drives it.
3045
+ const replacedGoal = goalStates.get(sessionID)
2890
3046
  sessionOrdered.delete(sessionID)
2891
3047
  cleanupGoal(sessionID)
2892
3048
  lastGoalResults.delete(sessionID)
2893
3049
  registerSessionGoal(goal)
2894
3050
  focusGoal(sessionID, goal)
2895
3051
  await persist(sessionID)
3052
+ announceLifecycle(sessionID, replacedGoal ? "Goal replaced and active." : "Goal active.", {
3053
+ goal,
3054
+ transition: replacedGoal ? "replaced-active" : "active",
3055
+ expectedState: "active",
3056
+ })
2896
3057
  // Escape in the tool result only: goal.condition is stored raw so callers
2897
3058
  // that build XML (buildGoalBlock, buildContinueMessage) can apply escaping
2898
3059
  // themselves. Escaping here prevents XML metacharacters in user-supplied
@@ -2920,6 +3081,7 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
2920
3081
  }
2921
3082
 
2922
3083
  const messages = []
3084
+ let lifecycleNotice = null
2923
3085
 
2924
3086
  if (typeof args.objective === "string" && args.objective.trim()) {
2925
3087
  if (args.objective.trim().length > MAX_GOAL_OBJECTIVE_LENGTH) {
@@ -2938,6 +3100,13 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
2938
3100
  goal.lastStatus = "Goal objective updated."
2939
3101
  pushHistory(goal, "edited", `Objective updated to: ${summarizeText(goal.condition, 400)}`)
2940
3102
  messages.push(`Objective updated: ${escapeGoalText(goal.condition)}`)
3103
+ lifecycleNotice = {
3104
+ text: `Goal updated; state remains ${goalDisplayState(goal)}.`,
3105
+ transition: "updated",
3106
+ reason: goalDisplayState(goal),
3107
+ expectedState: goalDisplayState(goal),
3108
+ expectedStopReason: goal.stopped ? goal.stopReason : "",
3109
+ }
2941
3110
  }
2942
3111
 
2943
3112
  if (args.status !== undefined) {
@@ -2950,13 +3119,24 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
2950
3119
  if (!evidence) return "Completion evidence is required before a goal can be archived."
2951
3120
  if (evidence.length > MAX_LEGACY_EVIDENCE_LENGTH)
2952
3121
  return `Completion evidence must be ${MAX_LEGACY_EVIDENCE_LENGTH} characters or fewer.`
3122
+ const auditedGoalID = goal.goalId
3123
+ const auditedRunID = goal.runId
3124
+ if (auditMessagesEnabled) {
3125
+ await announceAudit(
3126
+ sessionID,
3127
+ "Auditing goal completion: checking submitted evidence before archiving.",
3128
+ )
3129
+ const goalAfterAnnouncement = activeGoal(sessionID, auditedGoalID, auditedRunID)
3130
+ if (!goalAfterAnnouncement) {
3131
+ return "Completion audit finished after the goal changed; completion was not recorded."
3132
+ }
3133
+ goal = goalAfterAnnouncement
3134
+ }
2953
3135
  // If a completion auditor is configured, run it before archiving so the
2954
3136
  // agent tool path has the same integrity gate as the [goal:complete] marker
2955
3137
  // path. Without this, an autonomous agent could bypass the auditor by
2956
3138
  // calling update_goal({status:"complete"}) instead of using the marker.
2957
3139
  if (completionAuditor) {
2958
- const auditedGoalID = goal.goalId
2959
- const auditedRunID = goal.runId
2960
3140
  let verdict
2961
3141
  try {
2962
3142
  verdict = await completionAuditor({ goal, sessionID, latestText: evidence })
@@ -2975,6 +3155,25 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
2975
3155
  goal.lastStatus = `Completion audit rejected: ${summarizeText(reason, 200)}. Address it, then run /${commandName} resume.`
2976
3156
  pushHistory(goal, "audit-rejected", `Agent tool completion audit rejected: ${summarizeText(reason, 300)}`)
2977
3157
  await persist(sessionID)
3158
+ const rejectedGoalAfterPersist = currentGoal(sessionID, auditedGoalID, auditedRunID)
3159
+ if (
3160
+ rejectedGoalAfterPersist !== goal ||
3161
+ !goal.stopped ||
3162
+ goal.stopReason !== "audit rejected"
3163
+ ) {
3164
+ return "Completion audit was rejected, but the goal changed while that state was persisted; current state was left untouched."
3165
+ }
3166
+ if (auditMessagesEnabled) {
3167
+ await announceAudit(sessionID, `Audit result: completion rejected — ${summarizeText(reason, 160)}.`)
3168
+ } else {
3169
+ announceLifecycle(sessionID, "Goal paused — completion audit rejected. Run status for details.", {
3170
+ goal,
3171
+ transition: "audit-rejected",
3172
+ reason,
3173
+ expectedState: "paused",
3174
+ expectedStopReason: "audit rejected",
3175
+ })
3176
+ }
2978
3177
  return `Completion audit rejected: ${summarizeText(reason, 200)}. Goal paused; use /${commandName} resume after addressing the issue.`
2979
3178
  }
2980
3179
  }
@@ -2985,16 +3184,72 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
2985
3184
  evidence ? `Marked complete via tool: ${summarizeText(evidence, 400)}` : "Marked complete via agent tool.",
2986
3185
  )
2987
3186
  const ordered = sessionOrdered.has(sessionID)
2988
- rememberGoalResult(sessionID, goal, "achieved", "", evidence)
3187
+ const completedResult = rememberGoalResult(sessionID, goal, "achieved", "", evidence)
2989
3188
  cleanupGoal(sessionID)
2990
3189
  // Advance an ordered sequence just like the marker path does.
2991
- if (ordered) promoteNextOrderedGoal(sessionID)
3190
+ const promoted = ordered ? promoteNextOrderedGoal(sessionID) : null
3191
+ const postCompletionSnapshot = captureFocusedGoalSnapshot(sessionID)
2992
3192
  const durable = await persistFinal(sessionID, "completion", ledgerDurable)
2993
3193
  if (durable === false) {
2994
- restoreAfterTerminalPersistenceFailure(sessionID, goal, { ordered })
2995
- return "Completion verified, but terminal state could not be persisted. Goal remains paused."
3194
+ const restored = restoreAfterTerminalPersistenceFailure(sessionID, goal, {
3195
+ ordered,
3196
+ expectedCurrentSnapshot: postCompletionSnapshot,
3197
+ expectedResult: completedResult,
3198
+ })
3199
+ if (auditMessagesEnabled) {
3200
+ await announceAudit(
3201
+ sessionID,
3202
+ restored
3203
+ ? "Audit result: completion verified, but storage failed; goal remains paused and was not archived."
3204
+ : "Audit result: completion verified, but its terminal write failed after goal state changed; current state was left untouched.",
3205
+ )
3206
+ } else {
3207
+ announceLifecycle(
3208
+ sessionID,
3209
+ restored
3210
+ ? "Goal paused — completion could not be recorded durably."
3211
+ : "Previous goal completion could not be confirmed durably after goal state changed.",
3212
+ restored
3213
+ ? {
3214
+ goal,
3215
+ transition: "terminal-persistence-failed",
3216
+ reason: goal.stopReason,
3217
+ expectedState: "paused",
3218
+ expectedStopReason: "terminal persistence failed",
3219
+ }
3220
+ : {
3221
+ transition: "terminal-persistence-raced",
3222
+ requireCurrent: false,
3223
+ },
3224
+ )
3225
+ }
3226
+ return restored
3227
+ ? "Completion verified, but terminal state could not be persisted. Goal remains paused."
3228
+ : "Completion verified, but its terminal state could not be persisted before the goal changed. Current state was left untouched."
2996
3229
  }
2997
- return "Goal marked complete and archived."
3230
+ const activePromoted = promoted
3231
+ ? activeGoal(sessionID, promoted.goalId, promoted.runId)
3232
+ : null
3233
+ if (auditMessagesEnabled) {
3234
+ await announceAudit(
3235
+ sessionID,
3236
+ activePromoted
3237
+ ? "Audit result: completion accepted — goal archived as achieved; next ordered goal active."
3238
+ : "Audit result: completion accepted — goal archived as achieved.",
3239
+ )
3240
+ } else {
3241
+ announceLifecycle(
3242
+ sessionID,
3243
+ activePromoted ? "Goal achieved; next ordered goal active." : "Goal achieved.",
3244
+ {
3245
+ goal: activePromoted || goal,
3246
+ transition: activePromoted ? "achieved-promoted" : "achieved",
3247
+ requireCurrent: Boolean(activePromoted),
3248
+ expectedState: activePromoted ? "active" : "",
3249
+ },
3250
+ )
3251
+ }
3252
+ return AGENT_COMPLETE_SUCCESS
2998
3253
  }
2999
3254
  if (status === "blocked") {
3000
3255
  const blockerText = typeof args.blocker === "string" ? args.blocker.trim() : ""
@@ -3002,18 +3257,80 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
3002
3257
  return "status 'blocked' requires a non-empty 'blocker' argument describing what is needed."
3003
3258
  if (blockerText.length > MAX_GOAL_BLOCKER_LENGTH)
3004
3259
  return `Blocker must be ${MAX_GOAL_BLOCKER_LENGTH} characters or fewer.`
3260
+ const blockedGoalID = goal.goalId
3261
+ const blockedRunID = goal.runId
3262
+ if (auditMessagesEnabled) {
3263
+ await announceAudit(
3264
+ sessionID,
3265
+ "Auditing goal blocker: checking the submitted blocker before pausing.",
3266
+ )
3267
+ const goalAfterAnnouncement = activeGoal(sessionID, blockedGoalID, blockedRunID)
3268
+ if (!goalAfterAnnouncement) {
3269
+ return "Blocker audit finished after the goal changed; blocked state was not recorded."
3270
+ }
3271
+ goal = goalAfterAnnouncement
3272
+ }
3005
3273
  goal.blockedReason = blockerText
3006
3274
  goal.stopped = true
3007
3275
  goal.stopReason = "blocked"
3008
3276
  goal.lastStatus = "Assistant reported blocked."
3009
- pushHistory(goal, "blocked", goal.blockedReason)
3010
- messages.push("Goal marked blocked.")
3277
+ const ledgerDurable = pushHistory(goal, "blocked", goal.blockedReason)
3278
+ messages.push(AGENT_BLOCK_SUCCESS)
3279
+ const durable = await persistFinal(sessionID, "blocked", ledgerDurable)
3280
+ const blockedGoalAfterPersist = currentGoal(sessionID, blockedGoalID, blockedRunID)
3281
+ if (blockedGoalAfterPersist !== goal || goal.stopReason !== "blocked") {
3282
+ return "Blocked state changed while persistence completed; blocked state was not reported."
3283
+ }
3284
+ if (durable === false) {
3285
+ goal.stopReason = "terminal persistence failed"
3286
+ goal.lastStatus = "Blocked state could not be persisted; goal remains paused."
3287
+ if (auditMessagesEnabled) {
3288
+ await announceAudit(
3289
+ sessionID,
3290
+ "Audit result: blocker recognized, but storage failed; goal remains paused.",
3291
+ )
3292
+ } else {
3293
+ announceLifecycle(sessionID, "Goal paused — blocked state could not be recorded durably.", {
3294
+ goal,
3295
+ transition: "terminal-persistence-failed",
3296
+ expectedState: "paused",
3297
+ expectedStopReason: "terminal persistence failed",
3298
+ })
3299
+ }
3300
+ return "Blocker recognized, but terminal state could not be persisted. Goal remains paused."
3301
+ }
3302
+ if (auditMessagesEnabled) {
3303
+ await announceAudit(
3304
+ sessionID,
3305
+ `Audit result: goal paused as blocked — ${summarizeText(blockerText, 160)}. Run /${commandName} resume after addressing it.`,
3306
+ )
3307
+ } else {
3308
+ announceLifecycle(sessionID, `Goal blocked. Run /${commandName} status for the reason.`, {
3309
+ goal,
3310
+ transition: "blocked",
3311
+ expectedState: "blocked",
3312
+ expectedStopReason: "blocked",
3313
+ })
3314
+ }
3315
+ return messages.join(" ")
3011
3316
  } else if (status === "paused") {
3012
- goal.stopped = true
3013
- goal.stopReason = "paused"
3014
- goal.lastStatus = "Goal paused."
3015
- pushHistory(goal, "paused", "Paused via agent tool.")
3016
- messages.push("Goal paused.")
3317
+ if (goal.stopped && goal.stopReason === "paused") {
3318
+ if (!messages.length) return "Goal is already paused."
3319
+ messages.push("Goal is already paused.")
3320
+ } else {
3321
+ goal.stopped = true
3322
+ goal.stopReason = "paused"
3323
+ goal.lastStatus = "Goal paused."
3324
+ pushHistory(goal, "paused", "Paused via agent tool.")
3325
+ messages.push("Goal paused.")
3326
+ lifecycleNotice = {
3327
+ text: "Goal paused.",
3328
+ transition: "paused",
3329
+ reason: goal.stopReason,
3330
+ expectedState: "paused",
3331
+ expectedStopReason: "paused",
3332
+ }
3333
+ }
3017
3334
  } else if (status === "resumed") {
3018
3335
  if (!goal.stopped)
3019
3336
  return "Goal is already running. Pause or stop it first if you want to reset the budget window."
@@ -3027,6 +3344,11 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
3027
3344
  goal.lastStatus = "Goal resumed with a fresh local budget."
3028
3345
  pushHistory(goal, "resumed", "Resumed via agent tool with a fresh local budget window.")
3029
3346
  messages.push("Goal resumed with fresh limits.")
3347
+ lifecycleNotice = {
3348
+ text: "Goal resumed with fresh limits.",
3349
+ transition: "resumed",
3350
+ expectedState: "active",
3351
+ }
3030
3352
  }
3031
3353
  }
3032
3354
 
@@ -3034,6 +3356,15 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
3034
3356
  return "Nothing to update. Provide `objective` and/or `status`."
3035
3357
  }
3036
3358
  await persist(sessionID)
3359
+ if (lifecycleNotice) {
3360
+ announceLifecycle(sessionID, lifecycleNotice.text, {
3361
+ goal,
3362
+ transition: lifecycleNotice.transition,
3363
+ reason: lifecycleNotice.reason,
3364
+ expectedState: lifecycleNotice.expectedState,
3365
+ expectedStopReason: lifecycleNotice.expectedStopReason,
3366
+ })
3367
+ }
3037
3368
  return messages.join(" ")
3038
3369
  }
3039
3370
 
@@ -3042,15 +3373,33 @@ function buildAgentToolHandlers({ defaultGoalOptions, persist, persistTerminalSt
3042
3373
  // focused goal + result. Without sessionGoals.delete, background goals added via
3043
3374
  // `/goal add` survive clear and resurrect as the focused goal on restart.
3044
3375
  // Record the clear in the ledger before cleanupGoal removes the goal object.
3045
- for (const goal of listSessionGoals(sessionID)) {
3046
- pushHistory(goal, "cleared", "Cleared via agent tool.")
3047
- }
3376
+ const goals = listSessionGoals(sessionID)
3377
+ const clearedGoal = goalStates.get(sessionID) || goals[0] || null
3378
+ const hadState = goals.length > 0 || lastGoalResults.has(sessionID)
3379
+ const ledgerDurable =
3380
+ goals.length > 0 &&
3381
+ goals.map((goal) => pushHistory(goal, "cleared", "Cleared via agent tool.")).every(Boolean)
3048
3382
  sessionOrdered.delete(sessionID)
3049
3383
  sessionGoals.delete(sessionID)
3050
3384
  cleanupGoal(sessionID)
3051
3385
  lastGoalResults.delete(sessionID)
3052
- await persistFinal(sessionID, "clear")
3053
- return "Goal cleared."
3386
+ const durable = await persistFinal(sessionID, "clear", ledgerDurable)
3387
+ const clearStillCurrent = !goalStates.has(sessionID) && listSessionGoals(sessionID).length === 0
3388
+ if (hadState && clearStillCurrent) {
3389
+ announceLifecycle(sessionID, durable === false
3390
+ ? "Goal cleared in memory, but storage failed; it may reappear after restart."
3391
+ : "Goal cleared.", {
3392
+ goal: clearedGoal,
3393
+ transition: durable === false ? "clear-persistence-failed" : "cleared",
3394
+ requireCurrent: false,
3395
+ })
3396
+ }
3397
+ if (!clearStillCurrent) {
3398
+ return "Clear persistence finished after goal state changed; current state was left untouched."
3399
+ }
3400
+ return durable === false
3401
+ ? "Goal cleared in memory, but terminal state could not be persisted. It may reappear after restart."
3402
+ : "Goal cleared."
3054
3403
  }
3055
3404
 
3056
3405
  return { getGoal, getGoalHistory, setGoal, updateGoal, clearGoal }
@@ -3175,8 +3524,22 @@ function buildAgentTools(
3175
3524
  return goalToolFailure("already_running", "Goal is already running.")
3176
3525
  }
3177
3526
  const message = await handlers.updateGoal(sessionID, args)
3178
- if (args.status === "complete" && currentGoal(sessionID)) {
3179
- return goalToolFailure("completion_rejected", message)
3527
+ if (args.status === "complete") {
3528
+ if (
3529
+ message !== AGENT_COMPLETE_SUCCESS ||
3530
+ currentGoal(sessionID, before.goalId, before.runId)
3531
+ ) {
3532
+ return goalToolFailure("completion_rejected", message)
3533
+ }
3534
+ }
3535
+ if (args.status === "blocked") {
3536
+ const after = currentGoal(sessionID, before.goalId, before.runId)
3537
+ if (after !== before) {
3538
+ return goalToolFailure("goal_changed", message)
3539
+ }
3540
+ if (message !== AGENT_BLOCK_SUCCESS || !after.stopped || after.stopReason !== "blocked") {
3541
+ return goalToolFailure("block_rejected", message)
3542
+ }
3180
3543
  }
3181
3544
  return goalToolSuccess(message)
3182
3545
  },
@@ -3296,8 +3659,14 @@ function formatGoalList(sessionID, commandName = "goal") {
3296
3659
  lines.push(`Goals (${goals.length})${sessionOrdered.has(sessionID) ? " — ordered sequence" : ""}:`)
3297
3660
  goals.forEach((goal, index) => {
3298
3661
  const marker = goal.goalId === focusedId ? "focused" : goal.stopped ? "background" : "idle"
3299
- const state = goal.stopped && goal.goalId !== focusedId ? ` — ${goal.stopReason || "stopped"}` : ""
3300
- lines.push(`${index + 1}. [${marker}] ${goal.condition}${state}`)
3662
+ const state = goalDisplayState(goal)
3663
+ const reason = state === "blocked"
3664
+ ? goal.blockedReason || goal.stopReason
3665
+ : goal.stopped
3666
+ ? goal.stopReason
3667
+ : ""
3668
+ const reasonText = reason ? ` (${summarizeText(reason, 160)})` : ""
3669
+ lines.push(`${index + 1}. [${marker}] ${goal.condition} — state: ${state}${reasonText}`)
3301
3670
  })
3302
3671
  lines.push(`Switch with \`/${commandName} focus <number>\`.`)
3303
3672
  } else {
@@ -3343,6 +3712,37 @@ async function defaultAuditMessenger(client, sessionID, text) {
3343
3712
  }
3344
3713
  }
3345
3714
 
3715
+ // High-signal lifecycle feedback uses the same non-blocking host surfaces as
3716
+ // audit notices, but remains a separate channel so callers can configure each
3717
+ // independently. Messages are normalized and bounded before they reach either
3718
+ // host API; goal objectives, evidence, and workspace paths are deliberately
3719
+ // excluded by transition call sites.
3720
+ async function defaultLifecycleMessenger(client, sessionID, text) {
3721
+ const message = summarizeText(text, 500)
3722
+ const warning = /\b(?:paused|blocked|recovered|failed|passive)\b/i.test(message)
3723
+ const success = /\b(?:achieved|completed)\b/i.test(message)
3724
+ if (client?.app?.log) {
3725
+ dispatchAdvisoryHostCall(() => client.app.log({
3726
+ body: {
3727
+ service: "opencode-goal-plugin",
3728
+ level: warning ? "warn" : "info",
3729
+ message,
3730
+ extra: { sessionID, kind: "goal-lifecycle" },
3731
+ },
3732
+ }))
3733
+ }
3734
+ if (client?.tui?.showToast) {
3735
+ dispatchAdvisoryHostCall(() => client.tui.showToast({
3736
+ body: {
3737
+ title: "Goal workflow",
3738
+ message,
3739
+ variant: warning ? "warning" : success ? "success" : "info",
3740
+ duration: 6000,
3741
+ },
3742
+ }))
3743
+ }
3744
+ }
3745
+
3346
3746
  // Completion auditor. When an auditor is configured, a [goal:complete]
3347
3747
  // is verified before the goal is archived: an approved verdict archives it, a
3348
3748
  // rejected verdict restores the goal (pauses it with the reason) instead of
@@ -3499,6 +3899,41 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
3499
3899
  return persistence.persistChain
3500
3900
  }
3501
3901
 
3902
+ const lifecycleMessagesEnabled = pluginOptions.lifecycleMessages !== false
3903
+ const lifecycleMessenger =
3904
+ typeof pluginOptions.lifecycleMessenger === "function"
3905
+ ? pluginOptions.lifecycleMessenger
3906
+ : (sessionID, text) => defaultLifecycleMessenger(client, sessionID, text)
3907
+ const announceLifecycle = (
3908
+ sessionID,
3909
+ text,
3910
+ {
3911
+ goal,
3912
+ transition = "state",
3913
+ reason = "",
3914
+ requireCurrent = true,
3915
+ expectedState = "",
3916
+ expectedStopReason = "",
3917
+ } = {},
3918
+ ) => {
3919
+ if (!lifecycleMessagesEnabled || !sessionID) return false
3920
+ if (requireCurrent && goal) {
3921
+ const current = goalStates.get(sessionID)
3922
+ if (current !== goal) return false
3923
+ if (expectedState && goalDisplayState(current) !== expectedState) return false
3924
+ if (expectedStopReason && current.stopReason !== expectedStopReason) return false
3925
+ }
3926
+ const message = summarizeText(text, 500)
3927
+ if (!message) return false
3928
+ dispatchAdvisoryHostCall(
3929
+ () => lifecycleMessenger(sessionID, message),
3930
+ (error) => {
3931
+ void logPluginError(client, "Failed to deliver goal lifecycle message", error).catch(() => {})
3932
+ },
3933
+ )
3934
+ return true
3935
+ }
3936
+
3502
3937
  const passiveLoadResult = (entry) => ({
3503
3938
  kind: "passive",
3504
3939
  code: SESSION_OWNED_ELSEWHERE,
@@ -3596,7 +4031,40 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
3596
4031
  const status = await loadPersistedSessionState(persistence, client, sessionID)
3597
4032
  if (runtime.disposed) return releaseDisposedSession()
3598
4033
  pruneGoalResults(defaultGoalOptions)
3599
- if (status === "loaded" || status === "missing" || status === "reconstructed") await persist(sessionID)
4034
+ if (
4035
+ status === "loaded" ||
4036
+ status === "missing" ||
4037
+ status === "reconstructed" ||
4038
+ status === "reconciled-blocked"
4039
+ ) await persist(sessionID)
4040
+ const recoveredGoal = goalStates.get(sessionID)
4041
+ if (recoveredGoal?.stopped && recoveredGoal.stopReason === "recovered after restart") {
4042
+ announceLifecycle(sessionID, `Goal recovered and paused. Run /${commandName} status, then /${commandName} resume when ready.`, {
4043
+ goal: recoveredGoal,
4044
+ transition: "recovered-paused",
4045
+ reason: recoveredGoal.stopReason,
4046
+ expectedState: "paused",
4047
+ expectedStopReason: "recovered after restart",
4048
+ })
4049
+ } else if (
4050
+ status === "reconciled-blocked" &&
4051
+ recoveredGoal?.stopped &&
4052
+ recoveredGoal.stopReason === "blocked"
4053
+ ) {
4054
+ announceLifecycle(sessionID, `Goal recovered as blocked. Run /${commandName} status for the reason.`, {
4055
+ goal: recoveredGoal,
4056
+ transition: "recovered-blocked",
4057
+ reason: recoveredGoal.blockedReason,
4058
+ expectedState: "blocked",
4059
+ expectedStopReason: "blocked",
4060
+ })
4061
+ } else if (recoveredGoal?.lastStatus === "Promoted as the next ordered goal.") {
4062
+ announceLifecycle(sessionID, "Goal state recovered; the next ordered goal is active.", {
4063
+ goal: recoveredGoal,
4064
+ transition: "recovered-promoted",
4065
+ expectedState: "active",
4066
+ })
4067
+ }
3600
4068
  if (runtime.disposed) return releaseDisposedSession()
3601
4069
  return ACTIVE_PERSISTENCE_OWNED
3602
4070
  } catch (error) {
@@ -3681,6 +4149,12 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
3681
4149
  reason: "owned verifier agent registration was not confirmed",
3682
4150
  })
3683
4151
  : null
4152
+ const completionAuditLabel =
4153
+ typeof pluginOptions.auditor === "function"
4154
+ ? "custom completion auditor"
4155
+ : pluginOptions.completionAudit
4156
+ ? "built-in independent verifier"
4157
+ : "evidence gate only (independent verifier off)"
3684
4158
 
3685
4159
  clearRuntimeState()
3686
4160
 
@@ -3689,6 +4163,10 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
3689
4163
  persist,
3690
4164
  persistTerminalState,
3691
4165
  completionAuditor,
4166
+ completionAuditLabel,
4167
+ announceAudit,
4168
+ auditMessagesEnabled,
4169
+ announceLifecycle,
3692
4170
  commandName,
3693
4171
  })
3694
4172
 
@@ -3714,6 +4192,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
3714
4192
  ) => {
3715
4193
  const goal = goalStates.get(sessionID)
3716
4194
  if (!goal) return false
4195
+ if (goal.stopped && goal.stopReason === reason) return false
3717
4196
  currentRuntime().continuationControllers.get(sessionID)?.abort()
3718
4197
  goal.stopped = true
3719
4198
  goal.stopReason = reason
@@ -3722,6 +4201,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
3722
4201
  pushHistory(goal, "paused", history)
3723
4202
  activeContinues.delete(sessionID)
3724
4203
  await persist(sessionID)
4204
+ announceLifecycle(sessionID, `Goal paused — ${summarizeText(reason, 160)}.`, {
4205
+ goal,
4206
+ transition: "paused",
4207
+ reason,
4208
+ expectedState: "paused",
4209
+ expectedStopReason: reason,
4210
+ })
3725
4211
  if (abortAccepted) await abortAcceptedContinuation(sessionID)
3726
4212
  return true
3727
4213
  }
@@ -3798,6 +4284,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
3798
4284
  goal.stopReason = "continuation claim persistence failed"
3799
4285
  goal.lastStatus = `Auto-continue paused because its source-turn claim could not be persisted. Run /${commandName} resume after fixing storage.`
3800
4286
  pushHistory(goal, "paused", "Paused because the durable continuation source claim could not be persisted.")
4287
+ announceLifecycle(sessionID, "Goal paused — continuation state could not be persisted.", {
4288
+ goal,
4289
+ transition: "continuation-persistence-failed",
4290
+ reason: goal.stopReason,
4291
+ expectedState: "paused",
4292
+ expectedStopReason: "continuation claim persistence failed",
4293
+ })
3801
4294
  return null
3802
4295
  }
3803
4296
  return goal
@@ -3992,7 +4485,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
3992
4485
  replaceCommandOutputText(
3993
4486
  output,
3994
4487
  goal
3995
- ? formatStatus(goal, commandName)
4488
+ ? formatStatus(goal, commandName, completionAuditLabel)
3996
4489
  : lastResult
3997
4490
  ? formatGoalResult(lastResult)
3998
4491
  : `No active goal. Set one with \`/${commandName} <condition>\`.`,
@@ -4033,15 +4526,35 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4033
4526
  // sessionGoals.delete clears ALL backgrounded goals so they do not
4034
4527
  // resurrect as the focused goal on restart (cleanupGoal only removes the
4035
4528
  // focused one; background goals from `/goal add` would survive otherwise).
4036
- for (const goal of listSessionGoals(sessionID)) {
4037
- pushHistory(goal, "cleared", "User cleared the goal.")
4038
- }
4529
+ const goals = listSessionGoals(sessionID)
4530
+ const clearedGoal = goalStates.get(sessionID) || goals[0] || null
4531
+ const hadState = goals.length > 0 || lastGoalResults.has(sessionID)
4532
+ const ledgerDurable =
4533
+ goals.length > 0 &&
4534
+ goals.map((goal) => pushHistory(goal, "cleared", "User cleared the goal.")).every(Boolean)
4039
4535
  sessionOrdered.delete(sessionID)
4040
4536
  sessionGoals.delete(sessionID)
4041
4537
  cleanupGoal(sessionID)
4042
4538
  lastGoalResults.delete(sessionID)
4043
- await persist(sessionID)
4044
- replaceCommandOutputText(output, "Goal cleared.")
4539
+ const durable = await persistTerminalState(sessionID, "clear", ledgerDurable)
4540
+ const clearStillCurrent = !goalStates.has(sessionID) && listSessionGoals(sessionID).length === 0
4541
+ if (hadState && clearStillCurrent) {
4542
+ announceLifecycle(sessionID, durable === false
4543
+ ? "Goal cleared in memory, but storage failed; it may reappear after restart."
4544
+ : "Goal cleared.", {
4545
+ goal: clearedGoal,
4546
+ transition: durable === false ? "clear-persistence-failed" : "cleared",
4547
+ requireCurrent: false,
4548
+ })
4549
+ }
4550
+ replaceCommandOutputText(
4551
+ output,
4552
+ !clearStillCurrent
4553
+ ? "Clear persistence finished after goal state changed; current state was left untouched."
4554
+ : durable === false
4555
+ ? "Goal cleared in memory, but terminal state could not be persisted. It may reappear after restart."
4556
+ : "Goal cleared.",
4557
+ )
4045
4558
  return
4046
4559
  }
4047
4560
 
@@ -4051,6 +4564,10 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4051
4564
  replaceCommandOutputText(output, `No active goal. Set one with \`/${commandName} <condition>\`.`)
4052
4565
  return
4053
4566
  }
4567
+ if (goal.stopped && goal.stopReason === "paused") {
4568
+ replaceCommandOutputText(output, "Goal is already paused.")
4569
+ return
4570
+ }
4054
4571
  currentRuntime().continuationControllers.get(sessionID)?.abort()
4055
4572
  goal.stopped = true
4056
4573
  goal.stopReason = "paused"
@@ -4059,6 +4576,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4059
4576
  activeContinues.delete(sessionID)
4060
4577
  pushHistory(goal, "paused", "User paused the active goal.")
4061
4578
  await persist(sessionID)
4579
+ announceLifecycle(sessionID, "Goal paused.", {
4580
+ goal,
4581
+ transition: "paused",
4582
+ reason: goal.stopReason,
4583
+ expectedState: "paused",
4584
+ expectedStopReason: "paused",
4585
+ })
4062
4586
  await abortAcceptedContinuation(sessionID)
4063
4587
  replaceCommandOutputText(output, `Goal paused: ${goal.condition}`)
4064
4588
  return
@@ -4085,6 +4609,11 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4085
4609
  goal.lastStatus = "Goal resumed with a fresh local budget."
4086
4610
  pushHistory(goal, "resumed", "User resumed the goal with a fresh local budget window.")
4087
4611
  await persist(sessionID)
4612
+ announceLifecycle(sessionID, "Goal resumed with fresh limits.", {
4613
+ goal,
4614
+ transition: "resumed",
4615
+ expectedState: "active",
4616
+ })
4088
4617
  replaceCommandOutputText(output, `Goal resumed with fresh limits: ${goal.condition}`, {
4089
4618
  startsWork: true,
4090
4619
  })
@@ -4132,6 +4661,11 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4132
4661
  goal.lastStatus = "Goal objective updated."
4133
4662
  pushHistory(goal, "edited", `Objective updated to: ${summarizeText(newObjective, 400)}`)
4134
4663
  await persist(sessionID)
4664
+ announceLifecycle(sessionID, "Goal updated and active.", {
4665
+ goal,
4666
+ transition: "updated-active",
4667
+ expectedState: "active",
4668
+ })
4135
4669
  replaceCommandOutputText(
4136
4670
  output,
4137
4671
  [
@@ -4212,6 +4746,12 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4212
4746
  focusGoal(sessionID, firstGoal)
4213
4747
  sessionOrdered.add(sessionID)
4214
4748
  await persist(sessionID)
4749
+ announceLifecycle(sessionID, `Ordered goal sequence active (${objectives.length} goals).`, {
4750
+ goal: firstGoal,
4751
+ transition: "sequence-active",
4752
+ reason: String(objectives.length),
4753
+ expectedState: "active",
4754
+ })
4215
4755
  replaceCommandOutputText(
4216
4756
  output,
4217
4757
  [
@@ -4277,6 +4817,11 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4277
4817
  pushHistory(target, "focused", "Brought into focus as the session's active goal.")
4278
4818
  focusGoal(sessionID, target)
4279
4819
  await persist(sessionID)
4820
+ announceLifecycle(sessionID, "Goal focus changed; selected goal active.", {
4821
+ goal: target,
4822
+ transition: "focused-active",
4823
+ expectedState: "active",
4824
+ })
4280
4825
  replaceCommandOutputText(
4281
4826
  output,
4282
4827
  [
@@ -4335,6 +4880,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4335
4880
  registerSessionGoal(added)
4336
4881
  focusGoal(sessionID, added)
4337
4882
  await persist(sessionID)
4883
+ announceLifecycle(sessionID, current
4884
+ ? "Goal added and active; previous goal backgrounded."
4885
+ : "Goal added and active.", {
4886
+ goal: added,
4887
+ transition: current ? "added-active-backgrounded" : "added-active",
4888
+ expectedState: "active",
4889
+ })
4338
4890
  const total = listSessionGoals(sessionID).length
4339
4891
  replaceCommandOutputText(
4340
4892
  output,
@@ -4373,6 +4925,11 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4373
4925
  registerSessionGoal(goal)
4374
4926
  focusGoal(sessionID, goal)
4375
4927
  await persist(sessionID)
4928
+ announceLifecycle(sessionID, replacedGoal ? "Goal replaced and active." : "Goal active.", {
4929
+ goal,
4930
+ transition: replacedGoal ? "replaced-active" : "active",
4931
+ expectedState: "active",
4932
+ })
4376
4933
  replaceCommandOutputText(
4377
4934
  output,
4378
4935
  [
@@ -4740,7 +5297,23 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4740
5297
  auditedGoal.lastStatus = `Completion audit rejected: ${summarizeText(reason, 200)}. Address it, then run /${commandName} resume.`
4741
5298
  pushHistory(auditedGoal, "audit-rejected", `Completion audit rejected: ${summarizeText(reason, 300)}`)
4742
5299
  await persist(sessionID)
4743
- await announceAudit(sessionID, `Audit result: completion rejected — ${summarizeText(reason, 160)}.`)
5300
+ const rejectedGoalAfterPersist = currentGoal(sessionID, goalID, runID)
5301
+ if (
5302
+ rejectedGoalAfterPersist !== auditedGoal ||
5303
+ !auditedGoal.stopped ||
5304
+ auditedGoal.stopReason !== "audit rejected"
5305
+ ) return
5306
+ if (auditMessagesEnabled) {
5307
+ await announceAudit(sessionID, `Audit result: completion rejected — ${summarizeText(reason, 160)}.`)
5308
+ } else {
5309
+ announceLifecycle(sessionID, "Goal paused — completion audit rejected. Run status for details.", {
5310
+ goal: auditedGoal,
5311
+ transition: "audit-rejected",
5312
+ reason,
5313
+ expectedState: "paused",
5314
+ expectedStopReason: "audit rejected",
5315
+ })
5316
+ }
4744
5317
  return
4745
5318
  }
4746
5319
  pushHistory(
@@ -4760,23 +5333,80 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4760
5333
  `Assistant marked the goal complete with evidence: ${summarizeText(evidence, 400)}`,
4761
5334
  )
4762
5335
  const ordered = sessionOrdered.has(sessionID)
4763
- rememberGoalResult(sessionID, activeGoalAfterMessages, "achieved", "", evidence)
5336
+ const completedResult = rememberGoalResult(
5337
+ sessionID,
5338
+ activeGoalAfterMessages,
5339
+ "achieved",
5340
+ "",
5341
+ evidence,
5342
+ )
4764
5343
  cleanupGoal(sessionID)
4765
5344
  // Ordered sequence: auto-promote the next goal so the
4766
5345
  // session keeps working through the sequence without manual /goal focus.
4767
- if (ordered) {
4768
- promoteNextOrderedGoal(sessionID)
4769
- }
5346
+ const promoted = ordered ? promoteNextOrderedGoal(sessionID) : null
5347
+ const postCompletionSnapshot = captureFocusedGoalSnapshot(sessionID)
4770
5348
  const durable = await persistTerminalState(sessionID, "completion", ledgerDurable)
4771
5349
  if (durable === false) {
4772
- restoreAfterTerminalPersistenceFailure(sessionID, activeGoalAfterMessages, { ordered })
4773
- await announceAudit(
5350
+ const restored = restoreAfterTerminalPersistenceFailure(
4774
5351
  sessionID,
4775
- "Audit result: completion verified, but storage failed; goal remains paused and was not archived.",
5352
+ activeGoalAfterMessages,
5353
+ {
5354
+ ordered,
5355
+ expectedCurrentSnapshot: postCompletionSnapshot,
5356
+ expectedResult: completedResult,
5357
+ },
4776
5358
  )
5359
+ if (auditMessagesEnabled) {
5360
+ await announceAudit(
5361
+ sessionID,
5362
+ restored
5363
+ ? "Audit result: completion verified, but storage failed; goal remains paused and was not archived."
5364
+ : "Audit result: completion verified, but its terminal write failed after goal state changed; current state was left untouched.",
5365
+ )
5366
+ } else {
5367
+ announceLifecycle(
5368
+ sessionID,
5369
+ restored
5370
+ ? "Goal paused — completion could not be recorded durably."
5371
+ : "Previous goal completion could not be confirmed durably after goal state changed.",
5372
+ restored
5373
+ ? {
5374
+ goal: activeGoalAfterMessages,
5375
+ transition: "terminal-persistence-failed",
5376
+ reason: activeGoalAfterMessages.stopReason,
5377
+ expectedState: "paused",
5378
+ expectedStopReason: "terminal persistence failed",
5379
+ }
5380
+ : {
5381
+ transition: "terminal-persistence-raced",
5382
+ requireCurrent: false,
5383
+ },
5384
+ )
5385
+ }
4777
5386
  return
4778
5387
  }
4779
- await announceAudit(sessionID, "Audit result: completion accepted — goal archived as achieved.")
5388
+ const activePromoted = promoted
5389
+ ? activeGoal(sessionID, promoted.goalId, promoted.runId)
5390
+ : null
5391
+ if (auditMessagesEnabled) {
5392
+ await announceAudit(
5393
+ sessionID,
5394
+ activePromoted
5395
+ ? "Audit result: completion accepted — goal archived as achieved; next ordered goal active."
5396
+ : "Audit result: completion accepted — goal archived as achieved.",
5397
+ )
5398
+ } else {
5399
+ announceLifecycle(
5400
+ sessionID,
5401
+ activePromoted ? "Goal achieved; next ordered goal active." : "Goal achieved.",
5402
+ {
5403
+ goal: activePromoted || activeGoalAfterMessages,
5404
+ transition: activePromoted ? "achieved-promoted" : "achieved",
5405
+ requireCurrent: Boolean(activePromoted),
5406
+ expectedState: activePromoted ? "active" : "",
5407
+ },
5408
+ )
5409
+ }
4780
5410
  return
4781
5411
  }
4782
5412
  completionUnverified = true
@@ -4802,16 +5432,41 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4802
5432
  blockedGoal.stopReason = "blocked"
4803
5433
  const ledgerDurable = pushHistory(blockedGoal, "blocked", reason)
4804
5434
  const durable = await persistTerminalState(sessionID, "blocked", ledgerDurable)
5435
+ const blockedGoalAfterPersist = currentGoal(sessionID, goalID, runID)
5436
+ if (
5437
+ blockedGoalAfterPersist !== blockedGoal ||
5438
+ !blockedGoal.stopped ||
5439
+ blockedGoal.stopReason !== "blocked"
5440
+ ) return
4805
5441
  if (durable === false) {
4806
5442
  blockedGoal.stopReason = "terminal persistence failed"
4807
5443
  blockedGoal.lastStatus = "Blocked state could not be persisted; goal remains paused."
4808
- await announceAudit(sessionID, "Audit result: blocker recognized, but storage failed; goal remains paused.")
5444
+ if (auditMessagesEnabled) {
5445
+ await announceAudit(sessionID, "Audit result: blocker recognized, but storage failed; goal remains paused.")
5446
+ } else {
5447
+ announceLifecycle(sessionID, "Goal paused — blocked state could not be recorded durably.", {
5448
+ goal: blockedGoal,
5449
+ transition: "terminal-persistence-failed",
5450
+ reason: blockedGoal.stopReason,
5451
+ expectedState: "paused",
5452
+ expectedStopReason: "terminal persistence failed",
5453
+ })
5454
+ }
4809
5455
  return
4810
5456
  }
4811
- await announceAudit(
4812
- sessionID,
4813
- `Audit result: goal paused as blocked — ${summarizeText(reason, 160)}. Run /${commandName} resume after addressing it.`,
4814
- )
5457
+ if (auditMessagesEnabled) {
5458
+ await announceAudit(
5459
+ sessionID,
5460
+ `Audit result: goal paused as blocked — ${summarizeText(reason, 160)}. Run /${commandName} resume after addressing it.`,
5461
+ )
5462
+ } else {
5463
+ announceLifecycle(sessionID, `Goal blocked. Run /${commandName} status for the reason.`, {
5464
+ goal: blockedGoal,
5465
+ transition: "blocked",
5466
+ expectedState: "blocked",
5467
+ expectedStopReason: "blocked",
5468
+ })
5469
+ }
4815
5470
  return
4816
5471
  }
4817
5472
  blockerUnstated = true
@@ -4826,6 +5481,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4826
5481
 
4827
5482
  const limitReason = stopReason(activeGoalAfterMessages)
4828
5483
  if (limitReason) {
5484
+ let lifecycleAnnounced = false
4829
5485
  if (!activeGoalAfterMessages.budgetWrapupSent) {
4830
5486
  const claimedGoal = await claimContinuationSource(
4831
5487
  sessionID,
@@ -4842,6 +5498,17 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4842
5498
  claimedGoal.lastStatus = `${limitReason}; requested final handoff.`
4843
5499
  pushHistory(claimedGoal, "limit", `${limitReason}; requested a final handoff.`)
4844
5500
  await persist(sessionID)
5501
+ lifecycleAnnounced = announceLifecycle(
5502
+ sessionID,
5503
+ `Goal paused — ${summarizeText(limitReason, 160)}; final handoff requested.`,
5504
+ {
5505
+ goal: claimedGoal,
5506
+ transition: "limit-paused",
5507
+ reason: limitReason,
5508
+ expectedState: "paused",
5509
+ expectedStopReason: limitReason,
5510
+ },
5511
+ )
4845
5512
  currentRuntime().promptInFlightSessions.add(sessionID)
4846
5513
  let response
4847
5514
  try {
@@ -4868,6 +5535,15 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4868
5535
  pushHistory(activeGoalAfterMessages, "limit", limitReason)
4869
5536
  }
4870
5537
  await persist(sessionID)
5538
+ if (!lifecycleAnnounced) {
5539
+ announceLifecycle(sessionID, `Goal paused — ${summarizeText(limitReason, 160)}; final handoff requested.`, {
5540
+ goal: activeGoalAfterMessages,
5541
+ transition: "limit-paused",
5542
+ reason: limitReason,
5543
+ expectedState: "paused",
5544
+ expectedStopReason: limitReason,
5545
+ })
5546
+ }
4871
5547
  return
4872
5548
  }
4873
5549
 
@@ -4923,6 +5599,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4923
5599
  `Paused after ${activeGoalAfterMessages.noProgressTurns} low-progress turn(s) below ${activeGoalAfterMessages.options.noProgressTokenThreshold} output tokens.`,
4924
5600
  )
4925
5601
  await persist(sessionID)
5602
+ announceLifecycle(sessionID, "Goal paused — no progress threshold reached.", {
5603
+ goal: activeGoalAfterMessages,
5604
+ transition: "no-progress-paused",
5605
+ reason: activeGoalAfterMessages.stopReason,
5606
+ expectedState: "paused",
5607
+ expectedStopReason: "no progress",
5608
+ })
4926
5609
  return
4927
5610
  }
4928
5611
 
@@ -4968,6 +5651,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
4968
5651
  `Paused after ${activeGoalAfterMessages.noToolCallTurns} continuation turn(s) that produced no tool calls.`,
4969
5652
  )
4970
5653
  await persist(sessionID)
5654
+ announceLifecycle(sessionID, "Goal paused — no-tool-call threshold reached.", {
5655
+ goal: activeGoalAfterMessages,
5656
+ transition: "no-tool-calls-paused",
5657
+ reason: activeGoalAfterMessages.stopReason,
5658
+ expectedState: "paused",
5659
+ expectedStopReason: "no tool calls",
5660
+ })
4971
5661
  return
4972
5662
  }
4973
5663
 
@@ -5017,6 +5707,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5017
5707
  // the hard-limit path which also persists before its promptAsync call.
5018
5708
  pushHistory(activeGoalBeforePrompt, "budget-wrapup", "Budget threshold reached; sending final handoff prompt.")
5019
5709
  await persist(sessionID)
5710
+ announceLifecycle(sessionID, "Goal paused — budget threshold reached; final handoff requested.", {
5711
+ goal: activeGoalBeforePrompt,
5712
+ transition: "budget-wrapup-paused",
5713
+ reason: activeGoalBeforePrompt.stopReason,
5714
+ expectedState: "paused",
5715
+ expectedStopReason: "budget wrap-up requested",
5716
+ })
5020
5717
  }
5021
5718
 
5022
5719
  activeGoalBeforePrompt.turnCount += 1
@@ -5057,6 +5754,13 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5057
5754
  `Paused after ${activeGoalBeforePrompt.formatFailures} consecutive format-validation failure(s).`,
5058
5755
  )
5059
5756
  await persist(sessionID)
5757
+ announceLifecycle(sessionID, "Goal paused — repeated completion/blocker format failures.", {
5758
+ goal: activeGoalBeforePrompt,
5759
+ transition: "format-failures-paused",
5760
+ reason: activeGoalBeforePrompt.stopReason,
5761
+ expectedState: "paused",
5762
+ expectedStopReason: "format validation failures",
5763
+ })
5060
5764
  return
5061
5765
  }
5062
5766
  }
@@ -5081,6 +5785,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5081
5785
  currentRuntime().promptInFlightSessions.delete(sessionID)
5082
5786
  }
5083
5787
 
5788
+ let promptFailurePausedGoal = null
5084
5789
  if (response.error) {
5085
5790
  const activeGoalAfterPrompt = currentGoal(sessionID, goalID, runID)
5086
5791
  const message = `Auto-continue failed: ${response.error.name || "unknown error"}`
@@ -5096,6 +5801,7 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5096
5801
  activeGoalAfterPrompt.stopped = true
5097
5802
  activeGoalAfterPrompt.stopReason = "auto-continue failures"
5098
5803
  activeGoalAfterPrompt.lastStatus = `${message}; paused after ${activeGoalAfterPrompt.promptFailures} failure(s). Run /${commandName} resume to retry.`
5804
+ promptFailurePausedGoal = activeGoalAfterPrompt
5099
5805
  }
5100
5806
  }
5101
5807
  await logPluginError(client, message, response.error)
@@ -5119,6 +5825,15 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5119
5825
  }
5120
5826
  }
5121
5827
  await persist(sessionID)
5828
+ if (promptFailurePausedGoal) {
5829
+ announceLifecycle(sessionID, "Goal paused — repeated auto-continue failures.", {
5830
+ goal: promptFailurePausedGoal,
5831
+ transition: "prompt-failures-paused",
5832
+ reason: promptFailurePausedGoal.stopReason,
5833
+ expectedState: "paused",
5834
+ expectedStopReason: "auto-continue failures",
5835
+ })
5836
+ }
5122
5837
  } catch (error) {
5123
5838
  const activeGoalAfterError = currentGoal(sessionID, goalID, runID)
5124
5839
  if (activeGoalAfterError) {
@@ -5139,6 +5854,15 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
5139
5854
  activeGoalAfterError.lastStatus = `${message}; paused after ${activeGoalAfterError.promptFailures} failure(s). Run /${commandName} resume to retry.`
5140
5855
  }
5141
5856
  await persist(sessionID)
5857
+ if (activeGoalAfterError.stopped && activeGoalAfterError.stopReason === "auto-continue failures") {
5858
+ announceLifecycle(sessionID, "Goal paused — repeated auto-continue failures.", {
5859
+ goal: activeGoalAfterError,
5860
+ transition: "prompt-failures-paused",
5861
+ reason: activeGoalAfterError.stopReason,
5862
+ expectedState: "paused",
5863
+ expectedStopReason: "auto-continue failures",
5864
+ })
5865
+ }
5142
5866
  }
5143
5867
  await logPluginError(client, "Auto-continue failed", error)
5144
5868
  } finally {
@@ -5357,6 +6081,7 @@ export const testInternals = {
5357
6081
  ledgerPathFor,
5358
6082
  setLedgerSink,
5359
6083
  defaultAuditMessenger,
6084
+ defaultLifecycleMessenger,
5360
6085
  buildAuditPrompt,
5361
6086
  parseAuditVerdict,
5362
6087
  createChildSessionAuditor,
@@ -5375,6 +6100,7 @@ export const testInternals = {
5375
6100
  extractCompletionEvidence,
5376
6101
  findLatestAssistantMessage,
5377
6102
  formatArgumentErrors,
6103
+ goalDisplayState,
5378
6104
  formatStatus,
5379
6105
  getSessionID,
5380
6106
  goalIsBlocked,