@possumtech/rummy 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/.env.example +40 -15
  2. package/.xai.key +1 -0
  3. package/PLUGINS.md +169 -53
  4. package/README.md +38 -32
  5. package/SPEC.md +366 -179
  6. package/bin/digest.js +1097 -0
  7. package/biome/no-fallbacks.grit +2 -2
  8. package/gemini.key +1 -0
  9. package/lang/en.json +10 -1
  10. package/migrations/001_initial_schema.sql +9 -2
  11. package/package.json +19 -8
  12. package/service.js +1 -0
  13. package/src/agent/AgentLoop.js +76 -26
  14. package/src/agent/ContextAssembler.js +2 -0
  15. package/src/agent/Entries.js +238 -60
  16. package/src/agent/ProjectAgent.js +44 -0
  17. package/src/agent/TurnExecutor.js +99 -30
  18. package/src/agent/XmlParser.js +206 -111
  19. package/src/agent/errors.js +35 -0
  20. package/src/agent/known_queries.sql +1 -1
  21. package/src/agent/known_store.sql +3 -42
  22. package/src/agent/materializeContext.js +30 -1
  23. package/src/agent/runs.sql +8 -18
  24. package/src/agent/tokens.js +0 -1
  25. package/src/agent/turns.sql +1 -0
  26. package/src/hooks/Hooks.js +26 -0
  27. package/src/hooks/RummyContext.js +12 -1
  28. package/src/lib/hedberg/README.md +60 -0
  29. package/src/lib/hedberg/hedberg.js +60 -0
  30. package/src/lib/hedberg/marker.js +158 -0
  31. package/src/{plugins → lib}/hedberg/matcher.js +1 -2
  32. package/src/llm/LlmProvider.js +41 -3
  33. package/src/llm/openaiStream.js +17 -0
  34. package/src/plugins/ask_user/ask_user.js +12 -2
  35. package/src/plugins/ask_user/ask_userDoc.md +1 -5
  36. package/src/plugins/budget/README.md +29 -24
  37. package/src/plugins/budget/budget.js +166 -110
  38. package/src/plugins/cli/README.md +3 -4
  39. package/src/plugins/cli/cli.js +31 -5
  40. package/src/plugins/cloudflare/cloudflare.js +136 -0
  41. package/src/plugins/cp/cp.js +41 -4
  42. package/src/plugins/cp/cpDoc.md +5 -6
  43. package/src/plugins/engine/engine.sql +1 -1
  44. package/src/plugins/env/README.md +5 -4
  45. package/src/plugins/env/env.js +7 -4
  46. package/src/plugins/env/envDoc.md +7 -8
  47. package/src/plugins/error/error.js +56 -15
  48. package/src/plugins/file/README.md +12 -3
  49. package/src/plugins/file/file.js +2 -2
  50. package/src/plugins/get/get.js +59 -36
  51. package/src/plugins/get/getDoc.md +10 -34
  52. package/src/plugins/google/google.js +115 -0
  53. package/src/plugins/hedberg/hedberg.js +13 -56
  54. package/src/plugins/helpers.js +66 -12
  55. package/src/plugins/index.js +1 -2
  56. package/src/plugins/instructions/README.md +44 -47
  57. package/src/plugins/instructions/instructions-system.md +44 -0
  58. package/src/plugins/instructions/instructions-user.md +53 -0
  59. package/src/plugins/instructions/instructions.js +58 -189
  60. package/src/plugins/known/README.md +6 -7
  61. package/src/plugins/known/known.js +24 -30
  62. package/src/plugins/log/log.js +41 -32
  63. package/src/plugins/mv/mv.js +40 -1
  64. package/src/plugins/mv/mvDoc.md +1 -8
  65. package/src/plugins/ollama/ollama.js +4 -3
  66. package/src/plugins/openai/openai.js +4 -3
  67. package/src/plugins/openrouter/openrouter.js +14 -4
  68. package/src/plugins/persona/README.md +11 -13
  69. package/src/plugins/persona/default.md +29 -0
  70. package/src/plugins/persona/persona.js +10 -66
  71. package/src/plugins/policy/policy.js +23 -22
  72. package/src/plugins/prompt/README.md +37 -27
  73. package/src/plugins/prompt/prompt.js +13 -19
  74. package/src/plugins/rm/rm.js +18 -0
  75. package/src/plugins/rm/rmDoc.md +5 -6
  76. package/src/plugins/rpc/rpc.js +3 -3
  77. package/src/plugins/set/set.js +205 -323
  78. package/src/plugins/set/setDoc.md +47 -17
  79. package/src/plugins/sh/README.md +6 -5
  80. package/src/plugins/sh/sh.js +8 -5
  81. package/src/plugins/sh/shDoc.md +7 -8
  82. package/src/plugins/skill/README.md +37 -14
  83. package/src/plugins/skill/skill.js +200 -101
  84. package/src/plugins/skill/skillDoc.js +3 -0
  85. package/src/plugins/skill/skillDoc.md +9 -0
  86. package/src/plugins/stream/README.md +7 -6
  87. package/src/plugins/stream/finalize.js +100 -0
  88. package/src/plugins/stream/stream.js +13 -45
  89. package/src/plugins/telemetry/telemetry.js +27 -4
  90. package/src/plugins/think/think.js +2 -3
  91. package/src/plugins/think/thinkDoc.md +2 -4
  92. package/src/plugins/unknown/README.md +1 -1
  93. package/src/plugins/unknown/unknown.js +17 -19
  94. package/src/plugins/update/update.js +4 -51
  95. package/src/plugins/update/updateDoc.md +21 -6
  96. package/src/plugins/xai/xai.js +68 -102
  97. package/src/plugins/yolo/yolo.js +102 -75
  98. package/src/sql/functions/hedmatch.js +1 -1
  99. package/src/sql/functions/hedreplace.js +1 -1
  100. package/src/sql/functions/hedsearch.js +1 -1
  101. package/src/sql/functions/slugify.js +16 -2
  102. package/BENCH_ENVIRONMENT.md +0 -230
  103. package/CLIENT_INTERFACE.md +0 -396
  104. package/last_run.txt +0 -5617
  105. package/scriptify/ask_run.js +0 -77
  106. package/scriptify/cache_probe.js +0 -66
  107. package/scriptify/cache_probe_grok.js +0 -74
  108. package/src/agent/budget.js +0 -33
  109. package/src/agent/config.js +0 -38
  110. package/src/plugins/hedberg/README.md +0 -71
  111. package/src/plugins/hedberg/docs.md +0 -0
  112. package/src/plugins/hedberg/edits.js +0 -55
  113. package/src/plugins/hedberg/normalize.js +0 -17
  114. package/src/plugins/hedberg/sed.js +0 -49
  115. package/src/plugins/instructions/instructions.md +0 -34
  116. package/src/plugins/instructions/instructions_104.md +0 -8
  117. package/src/plugins/instructions/instructions_105.md +0 -39
  118. package/src/plugins/instructions/instructions_106.md +0 -22
  119. package/src/plugins/instructions/instructions_107.md +0 -17
  120. package/src/plugins/instructions/instructions_108.md +0 -0
  121. package/src/plugins/known/knownDoc.js +0 -3
  122. package/src/plugins/known/knownDoc.md +0 -8
  123. package/src/plugins/unknown/unknownDoc.js +0 -3
  124. package/src/plugins/unknown/unknownDoc.md +0 -11
  125. package/turns/cli_1777462658211/turn_001.txt +0 -772
  126. package/turns/cli_1777462658211/turn_002.txt +0 -606
  127. package/turns/cli_1777462658211/turn_003.txt +0 -667
  128. package/turns/cli_1777462658211/turn_004.txt +0 -297
  129. package/turns/cli_1777462658211/turn_005.txt +0 -301
  130. package/turns/cli_1777462658211/turn_006.txt +0 -262
  131. package/turns/cli_1777465095132/turn_001.txt +0 -715
  132. package/turns/cli_1777465095132/turn_002.txt +0 -236
  133. package/turns/cli_1777465095132/turn_003.txt +0 -287
  134. package/turns/cli_1777465095132/turn_004.txt +0 -694
  135. package/turns/cli_1777465095132/turn_005.txt +0 -422
  136. package/turns/cli_1777465095132/turn_006.txt +0 -365
  137. package/turns/cli_1777465095132/turn_007.txt +0 -885
  138. package/turns/cli_1777465095132/turn_008.txt +0 -1277
  139. package/turns/cli_1777465095132/turn_009.txt +0 -736
  140. /package/src/{plugins → lib}/hedberg/patterns.js +0 -0
@@ -1,4 +1,4 @@
1
- // No silent fallbacks outside hedberg.
1
+ // No silent fallbacks outside src/lib/hedberg/ + src/agent/XmlParser.js.
2
2
  // Rule: interiors crash on contract violation, boundaries validate.
3
3
  // Patterns like `|| 0`, `?? ""` silently mask missing data.
4
4
  // hedberg is the stochastic-interpretation boundary — fallbacks
@@ -39,7 +39,7 @@ or {
39
39
  `Number.parseFloat(process.env.$_) || $_`,
40
40
  `Number.parseFloat(process.env.$_) ?? $_`
41
41
  } as $match where {
42
- $filename <: not includes "src/plugins/hedberg/",
42
+ $filename <: not includes "src/lib/hedberg/",
43
43
  $filename <: not includes "src/agent/XmlParser.js",
44
44
  $filename <: not includes "/test/",
45
45
  $filename <: not includes ".test.js",
package/gemini.key ADDED
@@ -0,0 +1 @@
1
+ AIzaSyD4VBYwfo5wiVp0IIy368rlbEhJTnL2k2c
package/lang/en.json CHANGED
@@ -30,5 +30,14 @@
30
30
  "error.xai_base_url_missing": "xai/ model requested but XAI_BASE_URL is not set.",
31
31
  "error.xai_api_key_missing": "xai/ model requested but XAI_API_KEY is not set.",
32
32
  "error.xai_auth": "xAI Authentication Error: {status}. Please check your XAI_API_KEY.",
33
- "error.xai_api": "xAI API error: {status}"
33
+ "error.xai_api": "xAI API error: {status}",
34
+ "error.google_auth": "Google AI Studio authentication error: {status}. Check the gemini.key file in repo root.",
35
+ "error.google_api": "Google AI Studio API error: {status}",
36
+ "error.google_models_failed": "Google AI Studio /v1beta/models/{model} failed: {status}",
37
+ "error.google_no_context_length": "Google AI Studio model '{model}' returned no inputTokenLimit.",
38
+ "error.cloudflare_auth": "Cloudflare Workers AI authentication error: {status}. Check cloudflare.key and CLOUDFLARE_ACCOUNT_ID.",
39
+ "error.cloudflare_api": "Cloudflare Workers AI API error: {status}",
40
+ "error.cloudflare_models_failed": "Cloudflare /ai/models/search for '{model}' failed: {status}",
41
+ "error.cloudflare_model_not_found": "Cloudflare Workers AI model '{model}' not found in models-search results.",
42
+ "error.cloudflare_no_context_length": "Cloudflare Workers AI model '{model}' returned no context_window or max_input_tokens property."
34
43
  }
@@ -99,6 +99,13 @@ CREATE TABLE IF NOT EXISTS turns (
99
99
  , reasoning_tokens INTEGER NOT NULL DEFAULT 0 CHECK (reasoning_tokens >= 0)
100
100
  , total_tokens INTEGER NOT NULL DEFAULT 0 CHECK (total_tokens >= 0)
101
101
  , cost REAL NOT NULL DEFAULT 0 CHECK (cost >= 0)
102
+ -- Full response metadata from the provider — everything except
103
+ -- content/reasoning_content (those live on the assistant://N entry
104
+ -- and turns.reasoning_content respectively). Catches finish_reason,
105
+ -- system_fingerprint, response id, service_tier, raw usage object,
106
+ -- and whatever provider-specific fields land on the response. Kept
107
+ -- as opaque JSON so provider shape drift doesn't bite us.
108
+ , response_metadata JSON NOT NULL DEFAULT '{}' CHECK (json_valid(response_metadata))
102
109
  , created_at DATETIME DEFAULT CURRENT_TIMESTAMP
103
110
  );
104
111
  CREATE INDEX IF NOT EXISTS idx_turns_run_seq ON turns (run_id, sequence);
@@ -109,7 +116,7 @@ CREATE TABLE IF NOT EXISTS file_constraints (
109
116
  id INTEGER PRIMARY KEY AUTOINCREMENT
110
117
  , project_id INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE
111
118
  , pattern TEXT NOT NULL
112
- , visibility TEXT NOT NULL CHECK (visibility IN ('active', 'readonly', 'ignore'))
119
+ , visibility TEXT NOT NULL CHECK (visibility IN ('add', 'readonly', 'ignore'))
113
120
  , created_at DATETIME DEFAULT CURRENT_TIMESTAMP
114
121
  , UNIQUE (project_id, pattern)
115
122
  );
@@ -125,7 +132,7 @@ CREATE TABLE IF NOT EXISTS entries (
125
132
  , scope TEXT NOT NULL
126
133
  , path TEXT NOT NULL CHECK (length(path) <= 2048)
127
134
  , scheme TEXT GENERATED ALWAYS AS (schemeOf(path)) STORED
128
- , body TEXT NOT NULL DEFAULT ''
135
+ , body TEXT NOT NULL DEFAULT '' CHECK (length(body) <= $entry_size_max)
129
136
  , attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
130
137
  , hash TEXT
131
138
  , created_at DATETIME DEFAULT CURRENT_TIMESTAMP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@possumtech/rummy",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Relational Unknowns Memory Management Yoke",
5
5
  "keywords": [
6
6
  "llm"
@@ -40,24 +40,34 @@
40
40
  "test:all": "npm run lint && npm run test:unit && npm run test:intg && npm run test:e2e",
41
41
  "test:unit": "node --env-file-if-exists=.env.example --env-file-if-exists=.env.test --experimental-test-coverage --test-coverage-lines=50 --test-coverage-branches=50 --test-coverage-functions=50 --test-concurrency=1 --test-force-exit --test $(find src -name '*.test.js')",
42
42
  "test:intg": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test $(find test/integration -name '*.test.js')",
43
- "test:e2e": "mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/e2e -name '*.test.js') 2>&1 | tee /tmp/rummy_test_diag/e2e_$(date +%Y%m%dT%H%M%S).log",
44
- "test:live": "mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/live -name '*.test.js') 2>&1 | tee /tmp/rummy_test_diag/live_$(date +%Y%m%dT%H%M%S).log",
43
+ "test:e2e": "bash -c 'set -o pipefail; mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/e2e -name \"*.test.js\") 2>&1 | tee /tmp/rummy_test_diag/e2e_$(date +%Y%m%dT%H%M%S).log; status=$?; node bin/digest.js /tmp/rummy_test_diag/ 2>&1 | tail -3; exit $status'",
44
+ "test:live": "bash -c 'set -o pipefail; mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/live -name \"*.test.js\") 2>&1 | tee /tmp/rummy_test_diag/live_$(date +%Y%m%dT%H%M%S).log'",
45
45
  "test:clean": "rm -rf test/lme/results test/swe/results test/swe/repos test/tmp /tmp/rummy_test_diag /tmp/rummy_test_*.db /tmp/rummy_test_*.db-shm /tmp/rummy_test_*.db-wal && echo 'Test artifacts cleaned.'",
46
46
  "test:lme:get": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/lme/download.js",
47
- "test:lme": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/lme/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/lme_$(date +%Y%m%dT%H%M%S).log' --",
47
+ "test:lme": "bash -c 'set -o pipefail; mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/lme/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/lme_$(date +%Y%m%dT%H%M%S).log' --",
48
48
  "test:swe:setup": "bash test/swe/setup.sh",
49
49
  "test:swe:get": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/swe/download.js",
50
- "test:swe": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/swe/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/swe_$(date +%Y%m%dT%H%M%S).log' --",
50
+ "test:swe": "bash -c 'set -o pipefail; mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/swe/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/swe_$(date +%Y%m%dT%H%M%S).log' --",
51
51
  "test:swe:eval": "bash -c 'cd test/swe && source .venv/bin/activate && python evaluate.py \"$@\"' --",
52
52
  "test:swe:baseline": "bash -c 'cd test/swe && source .venv/bin/activate && python baseline.py \"$@\"' --",
53
53
  "test:lme:clean": "rm -rf test/lme/results/*/",
54
54
  "test:swe:clean": "rm -rf test/swe/results/*/ test/swe/repos/",
55
55
  "test:tbench:setup": "bash -c 'set -a; source .env.tbench; set +a; bash test/tbench/setup.sh'",
56
- "test:tbench": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.tbench test/tbench/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/tbench_$(date +%Y%m%dT%H%M%S).log' --",
56
+ "test:tbench": "bash -c 'echo \"Specify a profile: test:tbench:xfast | :gemma | :xfast_or\" >&2 && exit 64'",
57
+ "test:tbench:xfast": "bash -c 'set -o pipefail; mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.tbench --env-file-if-exists=.env.tbench.xfast test/tbench/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/tbench_xfast_$(date +%Y%m%dT%H%M%S).log' --",
58
+ "test:tbench:gemma": "bash -c 'set -o pipefail; mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.tbench --env-file-if-exists=.env.tbench.gemma test/tbench/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/tbench_gemma_$(date +%Y%m%dT%H%M%S).log' --",
59
+ "test:tbench:xfast_or": "bash -c 'set -o pipefail; mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.tbench --env-file-if-exists=.env.tbench.xfast_or test/tbench/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/tbench_xfast_or_$(date +%Y%m%dT%H%M%S).log' --",
60
+ "test:tbench:g43": "bash -c 'set -o pipefail; mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.tbench --env-file-if-exists=.env.tbench.g43 test/tbench/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/tbench_g43_$(date +%Y%m%dT%H%M%S).log' --",
57
61
  "test:tbench:clean": "rm -rf test/tbench/results/*/",
62
+ "test:tbench:summary": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.tbench test/tbench/summarize.js",
63
+ "test:programbench:setup": "bash test/programbench/setup.sh",
64
+ "test:programbench": "bash -c 'set -o pipefail; mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.tbench --env-file-if-exists=.env.tbench.gemma test/programbench/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/programbench_$(date +%Y%m%dT%H%M%S).log' --",
65
+ "test:programbench:eval": "bash -c 'cd test/programbench && . .venv/bin/activate && programbench eval \"$@\"' --",
66
+ "test:programbench:clean": "rm -rf test/programbench/results/*/",
58
67
  "test:clear": "rm -rf /tmp/rummy_test_diag /tmp/rummy_test_*.db /tmp/rummy_test_*.db-shm /tmp/rummy_test_*.db-wal /tmp/rummy-stories-*",
59
68
  "test:demo": "node --env-file-if-exists=.env.example --env-file-if-exists=.env bin/demo.js",
60
- "test:spec": "node test/spec-coverage.js"
69
+ "test:spec": "node test/spec-coverage.js",
70
+ "dev:digest": "bash -c 'DB=\"${1:-rummy_dev.db}\"; NAME=$(basename \"$DB\"); TMP=$(mktemp -d); sqlite3 \"$DB\" \".backup $TMP/$NAME\" && node bin/digest.js \"$TMP/$NAME\"; rm -rf \"$TMP\"' --"
61
71
  },
62
72
  "devDependencies": {
63
73
  "@biomejs/biome": "^2.4.6"
@@ -69,6 +79,7 @@
69
79
  "htmlparser2": "^12.0.0",
70
80
  "picomatch": "^4.0.4",
71
81
  "ws": "^8.19.0",
72
- "xpath": "^0.0.34"
82
+ "xpath": "^0.0.34",
83
+ "yauzl-promise": "^4.0.0"
73
84
  }
74
85
  }
package/service.js CHANGED
@@ -105,6 +105,7 @@ async function main() {
105
105
  functions: sqlFunctions,
106
106
  params: {
107
107
  mmap_size: Number(process.env.RUMMY_MMAP_MB) * 1024 * 1024,
108
+ entry_size_max: Number(process.env.RUMMY_ENTRY_SIZE_MAX),
108
109
  },
109
110
  });
110
111
 
@@ -1,5 +1,20 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
1
4
  import msg from "./messages.js";
2
5
 
6
+ const DEFAULT_PERSONA_PATH = join(
7
+ dirname(fileURLToPath(import.meta.url)),
8
+ "../plugins/persona/default.md",
9
+ );
10
+ let cachedDefaultPersona = null;
11
+ async function loadDefaultPersona() {
12
+ if (cachedDefaultPersona == null) {
13
+ cachedDefaultPersona = await readFile(DEFAULT_PERSONA_PATH, "utf8");
14
+ }
15
+ return cachedDefaultPersona;
16
+ }
17
+
3
18
  const HTTP_TO_RUN_STATE = {
4
19
  100: "proposed",
5
20
  102: "streaming",
@@ -23,6 +38,11 @@ export default class AgentLoop {
23
38
  this.#hooks = hooks;
24
39
  this.#turnExecutor = turnExecutor;
25
40
  this.#entries = entries;
41
+ hooks.run.wake.on(this.#onWake.bind(this));
42
+ }
43
+
44
+ async #onWake({ runAlias, body, mode }) {
45
+ await this.inject(runAlias, body, mode);
26
46
  }
27
47
 
28
48
  abort(runId) {
@@ -52,6 +72,7 @@ export default class AgentLoop {
52
72
  const s = await this.#db.get_run_summary.get({ id: runId });
53
73
  await hook.completed.emit({
54
74
  projectId,
75
+ runId,
55
76
  ...out,
56
77
  model: s.model,
57
78
  turns: s.turns,
@@ -113,10 +134,12 @@ export default class AgentLoop {
113
134
  const {
114
135
  fork: isFork = false,
115
136
  temperature = null,
116
- persona = null,
137
+ persona: personaOpt = null,
117
138
  contextLimit = null,
118
139
  } = options;
119
140
  const requestedModel = model;
141
+ let persona = personaOpt;
142
+ if (!persona) persona = await loadDefaultPersona();
120
143
 
121
144
  if (run && isFork) {
122
145
  const existingRun = await this.#db.get_run_by_alias.get({ alias: run });
@@ -133,6 +156,12 @@ export default class AgentLoop {
133
156
  context_limit: contextLimit,
134
157
  });
135
158
  await this.#entries.forkEntries(existingRun.id, runRow.id);
159
+ // Absolute turn numbering across the lineage; SPEC
160
+ // §budget_enforcement. Without this, the fork's first
161
+ // dispatch lands at turn 1 while inherited run_views carry
162
+ // parent-side turn values, and the budget grinder's
163
+ // `current_turn − 1` rule sees nothing meaningful.
164
+ await this.#entries.setNextTurn(runRow.id, existingRun.next_turn);
136
165
  await this.#writeRunEntry(runRow.id, alias, prompt, {
137
166
  projectId,
138
167
  parentRunId: existingRun.id,
@@ -468,14 +497,17 @@ export default class AgentLoop {
468
497
  `[LOOP] ${currentAlias} iter=${loopIteration} turn done: status=${result.status} turn=${result.turn}`,
469
498
  );
470
499
 
471
- const verdict = await this.#hooks.error.verdict({
472
- store: this.#entries,
473
- runId: currentRunId,
474
- loopId: currentLoopId,
475
- turn: result.turn,
476
- recorded: result.recorded,
477
- summaryText: result.summaryText,
478
- });
500
+ const verdict = await this.#hooks.turn.verdict.filter(
501
+ { continue: true },
502
+ {
503
+ store: this.#entries,
504
+ runId: currentRunId,
505
+ loopId: currentLoopId,
506
+ turn: result.turn,
507
+ recorded: result.recorded,
508
+ summaryText: result.summaryText,
509
+ },
510
+ );
479
511
  const vStatus = verdict.status === undefined ? "-" : verdict.status;
480
512
  const vReason = verdict.reason ? verdict.reason : "-";
481
513
  console.error(
@@ -661,6 +693,40 @@ export default class AgentLoop {
661
693
 
662
694
  const nextTurn = runRow.next_turn;
663
695
 
696
+ // Resolve the owning loop_id BEFORE writing the prompt entry so
697
+ // it lands with correct loop scope. Active run → reuse the
698
+ // running loop; otherwise enqueue the next loop and write the
699
+ // prompt with the new loop's id.
700
+ let loopId;
701
+ if (this.#activeRuns.has(runRow.id)) {
702
+ // Active runs have exactly one loop at status=102 by the
703
+ // loops table invariant — trust the contract.
704
+ const currentLoop = await this.#db.get_current_loop.get({
705
+ run_id: runRow.id,
706
+ });
707
+ loopId = currentLoop.id;
708
+ } else {
709
+ const injectLoopSeq = await this.#db.next_loop.get({
710
+ run_id: runRow.id,
711
+ });
712
+ const enqueued = await this.#db.enqueue_loop.get({
713
+ run_id: runRow.id,
714
+ sequence: injectLoopSeq.sequence,
715
+ mode,
716
+ model: runRow.model,
717
+ prompt: message,
718
+ config: JSON.stringify({
719
+ noRepo,
720
+ noInteraction,
721
+ noWeb,
722
+ noProposals,
723
+ yolo,
724
+ temperature: options?.temperature,
725
+ }),
726
+ });
727
+ loopId = enqueued.id;
728
+ }
729
+
664
730
  await this.#entries.set({
665
731
  runId: runRow.id,
666
732
  turn: nextTurn,
@@ -668,6 +734,7 @@ export default class AgentLoop {
668
734
  body: message,
669
735
  state: "resolved",
670
736
  attributes: { mode },
737
+ loopId,
671
738
  writer: "plugin",
672
739
  });
673
740
 
@@ -675,23 +742,6 @@ export default class AgentLoop {
675
742
  return { run: runAlias, status: runRow.status, injected: "next_turn" };
676
743
  }
677
744
 
678
- const injectLoopSeq = await this.#db.next_loop.get({ run_id: runRow.id });
679
- await this.#db.enqueue_loop.get({
680
- run_id: runRow.id,
681
- sequence: injectLoopSeq.sequence,
682
- mode,
683
- model: runRow.model,
684
- prompt: message,
685
- config: JSON.stringify({
686
- noRepo,
687
- noInteraction,
688
- noWeb,
689
- noProposals,
690
- yolo,
691
- temperature: options?.temperature,
692
- }),
693
- });
694
-
695
745
  const projectId = runRow.project_id;
696
746
  const project = await this.#db.get_project_by_id.get({ id: projectId });
697
747
  const controller = new AbortController();
@@ -9,6 +9,7 @@ export default class ContextAssembler {
9
9
  toolSet = null,
10
10
  lastContextTokens = 0,
11
11
  turn = 1,
12
+ persona = "",
12
13
  } = {},
13
14
  hooks,
14
15
  ) {
@@ -27,6 +28,7 @@ export default class ContextAssembler {
27
28
  lastContextTokens,
28
29
  toolSet,
29
30
  turn,
31
+ persona,
30
32
  };
31
33
 
32
34
  const system = await hooks.assembly.system.filter(systemPrompt, ctx);