pi-goal-list-loop-audit 0.35.66 → 0.35.68

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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.68 — bound-stop recovery (2026-08-26)
4
+
5
+ ### Fixed
6
+ Explicit `/loop resume` now recovers time- and token-bound stops as fresh
7
+ supervised windows without discarding iteration, history, or best-value
8
+ state. Recoverable stopped loops can accept a confirmed
9
+ `propose_loop_refine` change while remaining stopped until explicitly
10
+ resumed. Clean max-iteration and finished loops remain terminal; automatic
11
+ startup does not silently reset an explicit budget.
12
+
13
+ ### Tests
14
+ Coverage verifies fresh time windows, token-budget resets, preserved loop
15
+ history, stopped-loop refinement, and the unchanged max-iteration guard.
16
+
17
+ ## 0.35.67 — in-band provider-result recovery (2026-08-26)
18
+
19
+ ### Fixed
20
+ Repeated successful tool transports that carry a strong 503/429/network
21
+ provider pane no longer enter loop stuck or plateau accounting as ordinary
22
+ work. After the same tool/result fingerprint repeats, the turn is routed
23
+ through the existing provider-recovery envelope; one-off status text in a
24
+ searched document remains ordinary output.
25
+
26
+ ### Tests
27
+ Coverage spans provider-marker classification, repeated-pane detection,
28
+ loop-turn exemption, durable recovery parking, and the unchanged real-error
29
+ and repetition paths. The full release gate remains green.
30
+
3
31
  ## 0.35.66 — compiled-host auditor launcher (2026-08-26)
4
32
 
5
33
  ### Fixed
@@ -54,6 +54,23 @@ export function isLifecycleHeldLoopReason(reason?: string): boolean {
54
54
  || !!reason?.startsWith("send-retry storm:");
55
55
  }
56
56
 
57
+ /** A stopped loop can be respecified without discarding its history when the
58
+ * stop is a recoverable work failure or an explicit time/token window. Max
59
+ * iterations and clean/user stops remain terminal until a fresh `/loop start`.
60
+ */
61
+ export function isRefinableStoppedLoopReason(reason?: string): boolean {
62
+ return !!reason && (
63
+ reason.startsWith("time bound reached")
64
+ || reason.startsWith("token budget exhausted")
65
+ || reason.startsWith("stuck —")
66
+ || reason.startsWith("plateau —")
67
+ || reason.startsWith("metric never moved —")
68
+ || reason.startsWith("measure command broken —")
69
+ || reason.startsWith("provider errors —")
70
+ || reason.startsWith("stalled:")
71
+ );
72
+ }
73
+
57
74
  export interface LoopState {
58
75
  target: string;
59
76
  /** v0.23.0: optional — a metricless "spec loop" (measure=none) has no
@@ -113,8 +130,9 @@ export interface LoopState {
113
130
  recentPrints?: string[];
114
131
  /** v0.24.0: last few iteration texts (near-duplicate check + banned openings). */
115
132
  recentTexts?: string[];
116
- /** v0.24.0: rolling tool-result fingerprints {tool, hash, isError}. */
117
- recentToolResults?: { tool: string; hash: string; isError: boolean }[];
133
+ /** v0.24.0: rolling tool-result fingerprints {tool, hash, isError};
134
+ * providerFailure marks a repeated in-band provider/network pane. */
135
+ recentToolResults?: { tool: string; hash: string; isError: boolean; providerFailure?: boolean }[];
118
136
  /** v0.24.0: tool calls seen since the last completed iteration. */
119
137
  toolsThisTurn?: number;
120
138
  /** v0.24.0: consecutive iterations with zero tool calls. */
@@ -129,6 +129,18 @@ export interface ToolResultPrint {
129
129
  tool: string;
130
130
  hash: string;
131
131
  isError: boolean;
132
+ /** Successful tool transport can still contain a repeated provider failure. */
133
+ providerFailure?: boolean;
134
+ }
135
+
136
+ /** The repeated in-band provider pane is an outage signal, not loop work. */
137
+ export function repeatedInBandProviderFailure(results: ToolResultPrint[], repeat = REPETITION.toolResultRepeat): boolean {
138
+ if (repeat <= 0) return false;
139
+ const recent = results.slice(-repeat);
140
+ if (recent.length !== repeat || !recent[0]?.providerFailure) return false;
141
+ return recent.every((result) => result.providerFailure === true
142
+ && result.tool === recent[0]!.tool
143
+ && result.hash === recent[0]!.hash);
132
144
  }
133
145
 
134
146
  export interface LoopStuckInput {
@@ -939,6 +939,8 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
939
939
  !!r?.startsWith("plateau —") ||
940
940
  !!r?.startsWith("stalled:") ||
941
941
  !!r?.startsWith("stuck —") ||
942
+ !!r?.startsWith("time bound reached") ||
943
+ !!r?.startsWith("token budget exhausted") ||
942
944
  // v0.35.54 (collect-pass HIGH finding): the v0.35.31 "metric never
943
945
  // moved" stop message promises "/loop resume retries or /loop stop",
944
946
  // but this predicate never matched that prefix — the promised command
@@ -983,8 +985,33 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
983
985
  // An explicit resume re-arms the counters: fresh stall window,
984
986
  // cleared dead-turn/stuck streaks, reprieves restored — the user
985
987
  // saying "push again" wins over the ladder's memory (v0.29.19).
986
- state.loop = { ...stored, active: true, stopReason: undefined, consecutiveErrors: 0, consecutiveStuck: 0, lastStuckReason: undefined, stallCount: 0, auditPlateauReprieves: 0 };
988
+ // Time and token bounds are per supervised run window: resuming a
989
+ // bound-stopped loop preserves its iteration/history/best but starts a
990
+ // fresh elapsed-time window or token budget instead of stopping again
991
+ // on the same bound.
992
+ const resetTimeWindow = stored.stopReason?.startsWith("time bound reached") ?? false;
993
+ const resetTokenBudget = stored.stopReason?.startsWith("token budget exhausted") ?? false;
994
+ const resumedAt = nowIso();
995
+ state.loop = {
996
+ ...stored,
997
+ active: true,
998
+ stopReason: undefined,
999
+ consecutiveErrors: 0,
1000
+ consecutiveStuck: 0,
1001
+ lastStuckReason: undefined,
1002
+ stallCount: 0,
1003
+ auditPlateauReprieves: 0,
1004
+ ...(resetTimeWindow ? { startedAt: resumedAt } : {}),
1005
+ ...(resetTokenBudget ? { tokensUsed: 0 } : {}),
1006
+ };
987
1007
  persistState(ctx);
1008
+ if (resetTimeWindow || resetTokenBudget) {
1009
+ appendLedger(ctx.cwd, "loop_bound_window_reset", {
1010
+ timeWindow: resetTimeWindow,
1011
+ tokenBudget: resetTokenBudget,
1012
+ iteration: stored.iteration,
1013
+ });
1014
+ }
988
1015
  // v0.35.23 (note.md Next #2): an explicit resume is exactly the
989
1016
  // decision a load hold waits for — release it or the tick below
990
1017
  // would be frozen.
@@ -995,8 +1022,13 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
995
1022
  releaseContinuationDispatchStandDown();
996
1023
  releaseAuditorSurface();
997
1024
  scheduleLoopTick(ctx);
1025
+ const boundResetNote = resetTimeWindow
1026
+ ? " · fresh time window"
1027
+ : resetTokenBudget
1028
+ ? " · fresh token budget"
1029
+ : "";
998
1030
  ctx.ui.notify(
999
- `Loop resumed: iteration ${stored.iteration}/${stored.maxIterations > 0 ? stored.maxIterations : "∞"} · best ${stored.bestValue ?? "n/a"} — ${displaySlice(stored.target, 60)}`,
1031
+ `Loop resumed: iteration ${stored.iteration}/${stored.maxIterations > 0 ? stored.maxIterations : "∞"} · best ${stored.bestValue ?? "n/a"}${boundResetNote} — ${displaySlice(stored.target, 60)}`,
1000
1032
  "info",
1001
1033
  );
1002
1034
  return;
@@ -179,6 +179,7 @@ import {
179
179
  } from "../length-continue.js";
180
180
  import { isSubagentProviderFailure } from "../quota-retry.js";
181
181
  import {
182
+ classifyInBandProviderFailure,
182
183
  classifyMainModelFailure,
183
184
  isMainModelFallbackFailure,
184
185
  requiresMainModelRecovery,
@@ -229,6 +230,7 @@ import {
229
230
  import {
230
231
  REPETITION,
231
232
  isActuallyStuck,
233
+ repeatedInBandProviderFailure,
232
234
  loopInterventionDirective,
233
235
  continueVariant,
234
236
  textFingerprint,
@@ -456,6 +458,13 @@ const ZOMBIE_PAUSE_REASON = "automatic zero-stream abort — no provider activit
456
458
  // the park standing for manual resume — an honest degradation.
457
459
  let zombieRetryStreak: ZombieRetryStreak = { key: "", count: 0, lastAbortStreamAt: 0 };
458
460
  let zombieRetryTimer: NodeJS.Timeout | null = null;
461
+ // An in-band provider pane is observed during tool_result and consumed at the
462
+ // matching agent_end. Keep the raw text only in memory; the durable ledger
463
+ // records the bounded classification, never the provider payload.
464
+ let inBandProviderFailureRaw: string | null = null;
465
+ function clearInBandProviderFailure(): void {
466
+ inBandProviderFailureRaw = null;
467
+ }
459
468
 
460
469
  /** Arm the one-shot automatic re-dispatch after a successful zombie abort.
461
470
  * Returns true when a retry was scheduled (the caller adjusts its user-facing
@@ -951,11 +960,29 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
951
960
  const loop = state.loop!;
952
961
  const out = event?.output ?? event?.result ?? event?.details ?? "";
953
962
  const text = typeof out === "string" ? out : JSON.stringify(out) ?? "";
963
+ const tool = String(event?.toolName ?? "?");
964
+ const inBandFailure = classifyInBandProviderFailure(text);
954
965
  loop.recentToolResults = pushRepetitionCapped(
955
966
  loop.recentToolResults ?? [],
956
- { tool: String(event?.toolName ?? "?"), hash: textFingerprint(text), isError: Boolean(event?.isError ?? event?.error) },
967
+ {
968
+ tool,
969
+ hash: textFingerprint(text),
970
+ isError: Boolean(event?.isError ?? event?.error) || !!inBandFailure,
971
+ ...(inBandFailure ? { providerFailure: true } : {}),
972
+ },
957
973
  REPETITION.toolWindow,
958
974
  );
975
+ // Successful transport is not proof of successful work: only a stable
976
+ // repeated provider pane becomes a loop-level recovery signal. One-off
977
+ // 503/429 text in a searched document remains ordinary tool output.
978
+ if (inBandFailure && repeatedInBandProviderFailure(loop.recentToolResults)) {
979
+ inBandProviderFailureRaw = text.slice(0, 800);
980
+ appendLedger(eventCtx.cwd, "loop_in_band_provider_failure", {
981
+ tool,
982
+ kind: inBandFailure.kind,
983
+ repeats: REPETITION.toolResultRepeat,
984
+ });
985
+ }
959
986
  // v0.25.1: file-write progress signal for the multi-signal stuck
960
987
  // gate — a loop that is WRITING files is shipping, not stuck.
961
988
  if (isLoopWriteTool(String(event?.toolName ?? ""))) {
@@ -1034,6 +1061,7 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
1034
1061
  });
1035
1062
  }
1036
1063
  appendLedger(ctx.cwd, "session_shutdown", { reason: shutdownReason });
1064
+ clearInBandProviderFailure();
1037
1065
  markSessionOwnerShutdown(ctx.cwd, shutdownReason);
1038
1066
  writeSessionHandoff(ctx, shutdownReason);
1039
1067
  sessionReplacementUntil = Date.now() + SESSION_REBIND_GRACE_MS;
@@ -1136,6 +1164,7 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
1136
1164
  const startReason = typeof event?.reason === "string" ? event.reason : "unknown";
1137
1165
  initialSessionLoadPending = isBlankInitialStartup(ctx, startReason);
1138
1166
  rememberCtx(ctx);
1167
+ clearInBandProviderFailure();
1139
1168
  startHeartbeat();
1140
1169
  startUITicker();
1141
1170
  // v0.30.0: rebind bookkeeping — claim ownership, close any replacement
@@ -1744,10 +1773,10 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
1744
1773
  // v0.27.3: enrich lastA with text + priorText for the smarter nudge
1745
1774
  // accounting below.
1746
1775
  const assistants = (event.messages as any[]).filter((m: any) => m.role === "assistant");
1747
- const rawLastA = assistants.length ? assistants[assistants.length - 1] : null;
1776
+ let rawLastA = assistants.length ? assistants[assistants.length - 1] : null;
1748
1777
  const rawPriorA = assistants.length >= 2 ? assistants[assistants.length - 2] : null;
1749
1778
  const extractText = (m: any): string => (m && Array.isArray(m.content)) ? m.content.filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n") : "";
1750
- const lastA = rawLastA ? { stopReason: rawLastA.stopReason, text: extractText(rawLastA), priorText: extractText(rawPriorA) } : null;
1779
+ let lastA = rawLastA ? { stopReason: rawLastA.stopReason, text: extractText(rawLastA), priorText: extractText(rawPriorA) } : null;
1751
1780
  // v0.34.19: pi-ai clamps max_tokens to the remaining context before the
1752
1781
  // provider call. At ~99% context that clamp can be 1 token, which the
1753
1782
  // provider reports as stopReason "length" — but this is NOT an overlong
@@ -1835,6 +1864,23 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
1835
1864
  // never pass the same failure into main-goal recovery.
1836
1865
  return;
1837
1866
  }
1867
+ // A repeated provider pane arrived through a successful tool transport,
1868
+ // so pi reports an ordinary end_turn. Convert it into the same bounded
1869
+ // recovery envelope as a provider error before loop measurement/stuck
1870
+ // accounting can consume the dead turn.
1871
+ if (inBandProviderFailureRaw && isLoopActive()) {
1872
+ const raw = inBandProviderFailureRaw;
1873
+ clearInBandProviderFailure();
1874
+ const loop = state.loop!;
1875
+ // The normal loop error branch owns the consecutive-error counter and
1876
+ // its bounded recovery cap. Clearing the fingerprints here prevents the
1877
+ // same pane from being reclassified before that branch runs.
1878
+ loop.recentToolResults = [];
1879
+ rawLastA = { ...(rawLastA ?? {}), stopReason: "error", errorMessage: raw, content: [] };
1880
+ lastA = { stopReason: "error", text: raw, priorText: lastA?.priorText ?? "" };
1881
+ } else if (inBandProviderFailureRaw) {
1882
+ clearInBandProviderFailure();
1883
+ }
1838
1884
  if (await handleMainModelAgentEnd(ctx, rawLastA, lastA)) return;
1839
1885
  // v0.25.2: per-goal turn telemetry (/glla stats).
1840
1886
  if (state.goal && state.goal.status === "active") {
@@ -2292,6 +2338,7 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
2292
2338
  });
2293
2339
  pi.on("agent_start", (_event: any, ctx: ExtensionContext) => {
2294
2340
  rememberCtx(ctx);
2341
+ clearInBandProviderFailure();
2295
2342
  if (tryAbsorbHostSuccessor(ctx, "agent_start")) {
2296
2343
  ensureAgentToolsReady(ctx, true);
2297
2344
  return;
@@ -318,6 +318,7 @@ import {
318
318
  listAuditFanoutItemText,
319
319
  type LoopTickOutcome,
320
320
  HELD_ON_RESTORE,
321
+ isRefinableStoppedLoopReason,
321
322
  type LoopState,
322
323
  } from "../goal-loop-forever.js";
323
324
  import {
@@ -1928,7 +1929,7 @@ function registerAgentTools(pi: any): void {
1928
1929
  pi.registerTool(defineTool({
1929
1930
  name: "propose_loop_refine",
1930
1931
  label: "Propose loop spec refinement",
1931
- description: "While a loop is ACTIVE, propose refining the loop's spec — sharpen the target and/or change the measure command — when the current spec no longer captures 'better'. The user confirms; on a measure change the orchestrator test-runs the new command and re-baselines. Never edit the measure command or its inputs directly — that is gaming the metric.",
1932
+ description: "While a loop is active or safely stopped by a recoverable bound/failure, propose refining its spec — sharpen the target and/or change the measure command — when the current spec no longer captures 'better'. The user confirms; on a measure change the orchestrator test-runs the new command and re-baselines. Never edit the measure command or its inputs directly — that is gaming the metric.",
1932
1933
  parameters: Type.Object({
1933
1934
  target: Type.Optional(Type.String({ description: "The sharpened target text (omit to keep the current target)" })),
1934
1935
  measureCmd: Type.Optional(Type.String({ description: "The new measure command printing ONE number (omit to keep the current metric)" })),
@@ -1943,9 +1944,11 @@ function registerAgentTools(pi: any): void {
1943
1944
  const liveCtx = currentToolContext(execCtx);
1944
1945
  if (!liveCtx) return staleToolResult();
1945
1946
  const loop = state.loop;
1946
- if (!loop?.active) {
1947
- return { content: [{ type: "text", text: "No active loop to refine. propose_loop_refine is only valid while a loop is running." }], details: {} };
1947
+ const stoppedRefinable = !!loop && !loop.active && isRefinableStoppedLoopReason(loop.stopReason);
1948
+ if (!loop || (!loop.active && !stoppedRefinable)) {
1949
+ return { content: [{ type: "text", text: "No refinable loop is available. propose_loop_refine applies while a loop is running or after a recoverable bound/failure stop; clean max-iteration and user-finished loops require /loop start." }], details: {} };
1948
1950
  }
1951
+ const wasActive = loop.active;
1949
1952
  const newTarget = p.target?.trim() || loop.target;
1950
1953
  const newMeasure = p.measureCmd?.trim() || loop.measureCmd || "";
1951
1954
  // v0.23.0: a metricless loop can't be refined into a measured one
@@ -1989,7 +1992,7 @@ function registerAgentTools(pi: any): void {
1989
1992
  confirmed = (await confirmDraft(
1990
1993
  liveCtx,
1991
1994
  "Confirm loop spec refinement",
1992
- `Rationale: ${sanitizeDisplayText(p.rationale)}\n\nTarget:\n old: ${displaySlice(loop.target, 120)}\n new: ${displaySlice(newTarget, 120)}\n\nMeasure:\n old: ${sanitizeDisplayText(loop.measureCmd ?? "none")}\n new: ${sanitizeDisplayText(newMeasure)}${newMeasure !== loop.measureCmd ? `\n test-run: ${sanitizeDisplayText(testOutput).slice(0, 120)} → ${newBaseline}` : ""}${specChange ? `\n\nSpec file (${sanitizeDisplayText(loop.specFile ?? "")}:\n ${p.specText?.trim() ? `REPLACE with ${p.specText!.trim().length} chars` : ""}${p.specText?.trim() && p.specAppend?.trim() ? " + " : ""}${p.specAppend?.trim() ? `APPEND: ${sanitizeDisplayText(p.specAppend!.trim()).slice(0, 120)}` : ""}` : ""}\n\nThe loop keeps running against the refined spec (iteration ${loop.iteration} so far). Apply?`,
1995
+ `Rationale: ${sanitizeDisplayText(p.rationale)}\n\nTarget:\n old: ${displaySlice(loop.target, 120)}\n new: ${displaySlice(newTarget, 120)}\n\nMeasure:\n old: ${sanitizeDisplayText(loop.measureCmd ?? "none")}\n new: ${sanitizeDisplayText(newMeasure)}${newMeasure !== loop.measureCmd ? `\n test-run: ${sanitizeDisplayText(testOutput).slice(0, 120)} → ${newBaseline}` : ""}${specChange ? `\n\nSpec file (${sanitizeDisplayText(loop.specFile ?? "")}:\n ${p.specText?.trim() ? `REPLACE with ${p.specText!.trim().length} chars` : ""}${p.specText?.trim() && p.specAppend?.trim() ? " + " : ""}${p.specAppend?.trim() ? `APPEND: ${sanitizeDisplayText(p.specAppend!.trim()).slice(0, 120)}` : ""}` : ""}\n\nThe loop ${wasActive ? "keeps running" : "stays stopped until /loop resume"} against the refined spec (iteration ${loop.iteration} so far). Apply?`,
1993
1996
  )) === "yes";
1994
1997
  } catch {
1995
1998
  confirmed = false;
@@ -2026,8 +2029,10 @@ function registerAgentTools(pi: any): void {
2026
2029
  }
2027
2030
  persistState(liveCtx);
2028
2031
  appendLedger(liveCtx.cwd, "loop_refined", { iteration: loop.iteration, newTarget, newMeasureCmd: newMeasure, newBaseline, specChanged: specChange || undefined });
2029
- liveCtx.ui.notify(`Loop spec refined at iteration ${loop.iteration}.${newBaseline !== null ? ` New baseline: ${newBaseline}.` : ""}${specChange ? " Spec file updated." : ""}`, "info");
2030
- return { content: [{ type: "text", text: "Refinement confirmed and applied. Continue improving against the NEW spec — one small change per turn." }], details: {} };
2032
+ liveCtx.ui.notify(`Loop spec refined at iteration ${loop.iteration}.${newBaseline !== null ? ` New baseline: ${newBaseline}.` : ""}${specChange ? " Spec file updated." : ""}${wasActive ? "" : " Run /loop resume to continue with the preserved history."}`, "info");
2033
+ return { content: [{ type: "text", text: wasActive
2034
+ ? "Refinement confirmed and applied. Continue improving against the NEW spec — one small change per turn."
2035
+ : "Refinement confirmed and applied to the stopped loop. Run /loop resume to continue with the preserved history." }], details: {} };
2031
2036
  },
2032
2037
  }));
2033
2038
 
@@ -159,6 +159,20 @@ export function classifyMainModelFailure(error: string | undefined, opts?: { isC
159
159
  return { kind: "unknown", raw };
160
160
  }
161
161
 
162
+ /** A successful tool invocation can still carry a provider/network failure
163
+ * in its output. Only strong pane-shaped markers are eligible here; the loop
164
+ * caller additionally requires the same tool/result fingerprint to repeat
165
+ * before turning this into model recovery, so a one-off `503` in a searched
166
+ * document is not enough to park a loop. */
167
+ const IN_BAND_PROVIDER_FAILURE_PATTERN = /\b(?:http\s*)?(?:429|5\d\d)\b|rate[_ -]?limit|too many requests|network[_ -]?error|upstream(?:\s+(?:error|failure|unavailable))?|service unavailable|fetch failed|econn(?:reset|refused)|gateway(?:\s+(?:error|timeout))?/i;
168
+
169
+ export function classifyInBandProviderFailure(output: string | undefined): MainModelFailure | undefined {
170
+ const raw = typeof output === "string" ? output.trim() : "";
171
+ if (!raw || !IN_BAND_PROVIDER_FAILURE_PATTERN.test(raw)) return undefined;
172
+ const failure = classifyMainModelFailure(raw);
173
+ return failure.kind === "non-recoverable" ? undefined : failure;
174
+ }
175
+
162
176
  /** v0.34.116: detect when a length-context failure happened AFTER the
163
177
  * session_compact already failed. The classifier maps this to
164
178
  * `context-overflow` (rollback path: rotate to a larger-context ref). The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.35.66",
3
+ "version": "0.35.68",
4
4
  "description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "dracon",