opensquid 0.5.441 → 0.5.447
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/README.md +1 -0
- package/dist/functions/arm_scope.d.ts +27 -0
- package/dist/functions/arm_scope.d.ts.map +1 -0
- package/dist/functions/arm_scope.js +52 -0
- package/dist/functions/arm_scope.js.map +1 -0
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +1 -0
- package/dist/functions/index.js.map +1 -1
- package/dist/runtime/bootstrap.d.ts.map +1 -1
- package/dist/runtime/bootstrap.js +2 -0
- package/dist/runtime/bootstrap.js.map +1 -1
- package/dist/runtime/handoff/render.d.ts +5 -4
- package/dist/runtime/handoff/render.d.ts.map +1 -1
- package/dist/runtime/handoff/render.js +7 -7
- package/dist/runtime/handoff/render.js.map +1 -1
- package/dist/runtime/hooks/active_task_mirror.js +0 -0
- package/dist/runtime/hooks/apply_patch.js +0 -0
- package/dist/runtime/hooks/dispatch.js +0 -0
- package/dist/runtime/hooks/hook_output.js +0 -0
- package/dist/runtime/hooks/memory_reconcile.js +0 -0
- package/dist/runtime/hooks/new_project_detect.js +0 -0
- package/dist/runtime/hooks/profession_resolver.js +0 -0
- package/dist/runtime/hooks/scope_intent.js +0 -0
- package/dist/runtime/hooks/session_id.js +0 -0
- package/dist/runtime/hooks/session_liveness.js +0 -0
- package/dist/runtime/hooks/stop_drive.js +0 -0
- package/dist/runtime/hooks/stop_stream.js +0 -0
- package/dist/runtime/hooks/subagent_guard.js +0 -0
- package/dist/runtime/hooks/transcript.js +0 -0
- package/dist/runtime/hooks/transcript_tasks.js +0 -0
- package/dist/runtime/ralph/orchestrator.d.ts.map +1 -1
- package/dist/runtime/ralph/orchestrator.js +2 -1
- package/dist/runtime/ralph/orchestrator.js.map +1 -1
- package/dist/setup/cli/limits_state.d.ts.map +1 -1
- package/dist/setup/cli/limits_state.js +6 -40
- package/dist/setup/cli/limits_state.js.map +1 -1
- package/dist/setup/cli/pack_walk.d.ts +32 -0
- package/dist/setup/cli/pack_walk.d.ts.map +1 -0
- package/dist/setup/cli/pack_walk.js +76 -0
- package/dist/setup/cli/pack_walk.js.map +1 -0
- package/dist/setup/cli/permissions_state.d.ts.map +1 -1
- package/dist/setup/cli/permissions_state.js +6 -37
- package/dist/setup/cli/permissions_state.js.map +1 -1
- package/dist/setup/cli/triggers_state.d.ts.map +1 -1
- package/dist/setup/cli/triggers_state.js +3 -29
- package/dist/setup/cli/triggers_state.js.map +1 -1
- package/dist/workgraph/events.d.ts.map +1 -1
- package/dist/workgraph/events.js +10 -0
- package/dist/workgraph/events.js.map +1 -1
- package/dist/workgraph/store.d.ts.map +1 -1
- package/dist/workgraph/store.js +5 -0
- package/dist/workgraph/store.js.map +1 -1
- package/dist/workgraph/types.d.ts +2 -1
- package/dist/workgraph/types.d.ts.map +1 -1
- package/docs/ARCHITECTURE.md +268 -0
- package/package.json +5 -3
- package/packs/builtin/coding-flow/skills/entry-and-handoffs/skill.yaml +13 -17
- package/dist/anti-drift/evaluator.d.ts +0 -88
- package/dist/anti-drift/evaluator.d.ts.map +0 -1
- package/dist/anti-drift/evaluator.js +0 -417
- package/dist/anti-drift/evaluator.js.map +0 -1
- package/dist/anti-drift/evaluator.test.js +0 -78
- package/dist/anti-drift/rules.d.ts +0 -80
- package/dist/anti-drift/rules.d.ts.map +0 -1
- package/dist/anti-drift/rules.js +0 -368
- package/dist/anti-drift/rules.js.map +0 -1
- package/dist/anti-drift/rules.test.js +0 -213
- package/dist/anti-drift/state.d.ts +0 -107
- package/dist/anti-drift/state.d.ts.map +0 -1
- package/dist/anti-drift/state.js +0 -177
- package/dist/anti-drift/state.js.map +0 -1
- package/dist/anti-drift/state.test.js +0 -120
- package/dist/chat/adapters/discord.d.ts +0 -41
- package/dist/chat/adapters/discord.d.ts.map +0 -1
- package/dist/chat/adapters/discord.js +0 -176
- package/dist/chat/adapters/discord.js.map +0 -1
- package/dist/chat/adapters/discord.test.js +0 -25
- package/dist/chat/adapters/slack.d.ts +0 -43
- package/dist/chat/adapters/slack.d.ts.map +0 -1
- package/dist/chat/adapters/slack.js +0 -172
- package/dist/chat/adapters/slack.js.map +0 -1
- package/dist/chat/adapters/slack.test.js +0 -30
- package/dist/chat/adapters/telegram.d.ts +0 -148
- package/dist/chat/adapters/telegram.d.ts.map +0 -1
- package/dist/chat/adapters/telegram.js +0 -498
- package/dist/chat/adapters/telegram.js.map +0 -1
- package/dist/chat/adapters/telegram.test.js +0 -94
- package/dist/chat/config.d.ts +0 -98
- package/dist/chat/config.d.ts.map +0 -1
- package/dist/chat/config.js +0 -185
- package/dist/chat/config.js.map +0 -1
- package/dist/chat/daemon/active-project.d.ts +0 -17
- package/dist/chat/daemon/active-project.d.ts.map +0 -1
- package/dist/chat/daemon/active-project.js +0 -23
- package/dist/chat/daemon/active-project.js.map +0 -1
- package/dist/chat/daemon/autospawn.d.ts +0 -40
- package/dist/chat/daemon/autospawn.d.ts.map +0 -1
- package/dist/chat/daemon/autospawn.js +0 -129
- package/dist/chat/daemon/autospawn.js.map +0 -1
- package/dist/chat/daemon/autospawn.test.js +0 -112
- package/dist/chat/daemon/cli.d.ts +0 -18
- package/dist/chat/daemon/cli.d.ts.map +0 -1
- package/dist/chat/daemon/cli.js +0 -71
- package/dist/chat/daemon/cli.js.map +0 -1
- package/dist/chat/daemon/collisions.js +0 -384
- package/dist/chat/daemon/health-check.d.ts +0 -69
- package/dist/chat/daemon/health-check.d.ts.map +0 -1
- package/dist/chat/daemon/health-check.js +0 -112
- package/dist/chat/daemon/health-check.js.map +0 -1
- package/dist/chat/daemon/inbox-read.d.ts +0 -35
- package/dist/chat/daemon/inbox-read.d.ts.map +0 -1
- package/dist/chat/daemon/inbox-read.js +0 -75
- package/dist/chat/daemon/inbox-read.js.map +0 -1
- package/dist/chat/daemon/inbox-read.test.js +0 -97
- package/dist/chat/daemon/inbox.d.ts +0 -63
- package/dist/chat/daemon/inbox.d.ts.map +0 -1
- package/dist/chat/daemon/inbox.js +0 -56
- package/dist/chat/daemon/inbox.js.map +0 -1
- package/dist/chat/daemon/inbox.test.js +0 -110
- package/dist/chat/daemon/lifecycle.d.ts +0 -71
- package/dist/chat/daemon/lifecycle.d.ts.map +0 -1
- package/dist/chat/daemon/lifecycle.js +0 -221
- package/dist/chat/daemon/lifecycle.js.map +0 -1
- package/dist/chat/daemon/lifecycle.test.js +0 -163
- package/dist/chat/daemon/protocol.d.ts +0 -107
- package/dist/chat/daemon/protocol.d.ts.map +0 -1
- package/dist/chat/daemon/protocol.js +0 -54
- package/dist/chat/daemon/protocol.js.map +0 -1
- package/dist/chat/daemon/routing.d.ts +0 -140
- package/dist/chat/daemon/routing.d.ts.map +0 -1
- package/dist/chat/daemon/routing.js +0 -198
- package/dist/chat/daemon/routing.js.map +0 -1
- package/dist/chat/daemon/routing.test.js +0 -259
- package/dist/chat/daemon/rpc-client.d.ts +0 -45
- package/dist/chat/daemon/rpc-client.d.ts.map +0 -1
- package/dist/chat/daemon/rpc-client.js +0 -133
- package/dist/chat/daemon/rpc-client.js.map +0 -1
- package/dist/chat/daemon/rpc-server.d.ts +0 -39
- package/dist/chat/daemon/rpc-server.d.ts.map +0 -1
- package/dist/chat/daemon/rpc-server.js +0 -385
- package/dist/chat/daemon/rpc-server.js.map +0 -1
- package/dist/chat/daemon/rpc.test.js +0 -177
- package/dist/chat/daemon/subscribers.js +0 -257
- package/dist/chat/daemon/worker.d.ts +0 -27
- package/dist/chat/daemon/worker.d.ts.map +0 -1
- package/dist/chat/daemon/worker.js +0 -313
- package/dist/chat/daemon/worker.js.map +0 -1
- package/dist/chat/daemon/workspace-topic.js +0 -324
- package/dist/chat/env-token.d.ts +0 -60
- package/dist/chat/env-token.d.ts.map +0 -1
- package/dist/chat/env-token.js +0 -137
- package/dist/chat/env-token.js.map +0 -1
- package/dist/chat/env-token.test.js +0 -160
- package/dist/chat/factory.d.ts +0 -30
- package/dist/chat/factory.d.ts.map +0 -1
- package/dist/chat/factory.js +0 -50
- package/dist/chat/factory.js.map +0 -1
- package/dist/chat/factory.test.js +0 -55
- package/dist/chat/gateway.d.ts +0 -176
- package/dist/chat/gateway.d.ts.map +0 -1
- package/dist/chat/gateway.js +0 -146
- package/dist/chat/gateway.js.map +0 -1
- package/dist/chat/gateway.test.js +0 -192
- package/dist/claude-md.d.ts +0 -39
- package/dist/claude-md.d.ts.map +0 -1
- package/dist/claude-md.js +0 -113
- package/dist/claude-md.js.map +0 -1
- package/dist/claude-md.test.js +0 -91
- package/dist/codex/activate.d.ts +0 -66
- package/dist/codex/activate.d.ts.map +0 -1
- package/dist/codex/activate.js +0 -329
- package/dist/codex/activate.js.map +0 -1
- package/dist/codex/activate.test.js +0 -229
- package/dist/codex/bundled-default/bundled-default.test.js +0 -161
- package/dist/codex/cli-publish.test.js +0 -133
- package/dist/codex/cli.d.ts +0 -35
- package/dist/codex/cli.d.ts.map +0 -1
- package/dist/codex/cli.js +0 -554
- package/dist/codex/cli.js.map +0 -1
- package/dist/codex/cli.test.js +0 -277
- package/dist/codex/import-skill-md.d.ts +0 -53
- package/dist/codex/import-skill-md.d.ts.map +0 -1
- package/dist/codex/import-skill-md.js +0 -236
- package/dist/codex/import-skill-md.js.map +0 -1
- package/dist/codex/import-skill-md.test.js +0 -225
- package/dist/codex/loader.d.ts +0 -27
- package/dist/codex/loader.d.ts.map +0 -1
- package/dist/codex/loader.js +0 -86
- package/dist/codex/loader.js.map +0 -1
- package/dist/codex/loader.test.js +0 -75
- package/dist/codex/parse.d.ts +0 -28
- package/dist/codex/parse.d.ts.map +0 -1
- package/dist/codex/parse.js +0 -309
- package/dist/codex/parse.js.map +0 -1
- package/dist/codex/parse.test.js +0 -241
- package/dist/codex/store.d.ts +0 -87
- package/dist/codex/store.d.ts.map +0 -1
- package/dist/codex/store.js +0 -205
- package/dist/codex/store.js.map +0 -1
- package/dist/codex/store.test.js +0 -242
- package/dist/codex/types.d.ts +0 -398
- package/dist/codex/types.d.ts.map +0 -1
- package/dist/codex/types.js +0 -21
- package/dist/codex/types.js.map +0 -1
- package/dist/config.d.ts +0 -53
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -202
- package/dist/config.js.map +0 -1
- package/dist/config.test.js +0 -117
- package/dist/engine/cli.d.ts +0 -14
- package/dist/engine/cli.d.ts.map +0 -1
- package/dist/engine/cli.js +0 -171
- package/dist/engine/cli.js.map +0 -1
- package/dist/engine/client.d.ts +0 -219
- package/dist/engine/client.d.ts.map +0 -1
- package/dist/engine/client.js +0 -312
- package/dist/engine/client.js.map +0 -1
- package/dist/engine/config.d.ts +0 -62
- package/dist/engine/config.d.ts.map +0 -1
- package/dist/engine/config.js +0 -223
- package/dist/engine/config.js.map +0 -1
- package/dist/engine/index.d.ts +0 -17
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -16
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/resolver.d.ts +0 -62
- package/dist/engine/resolver.d.ts.map +0 -1
- package/dist/engine/resolver.js +0 -103
- package/dist/engine/resolver.js.map +0 -1
- package/dist/engine/singleton.d.ts +0 -95
- package/dist/engine/singleton.d.ts.map +0 -1
- package/dist/engine/singleton.js +0 -325
- package/dist/engine/singleton.js.map +0 -1
- package/dist/engine/types.d.ts +0 -402
- package/dist/engine/types.d.ts.map +0 -1
- package/dist/engine/types.js +0 -22
- package/dist/engine/types.js.map +0 -1
- package/dist/engine-binary-resolver.js +0 -110
- package/dist/engine-binary-resolver.test.js +0 -61
- package/dist/engine-cli.js +0 -60
- package/dist/engine-client.js +0 -301
- package/dist/engine-client.test.js +0 -118
- package/dist/functions/chain_state.d.ts +0 -51
- package/dist/functions/chain_state.d.ts.map +0 -1
- package/dist/functions/chain_state.js +0 -59
- package/dist/functions/chain_state.js.map +0 -1
- package/dist/hooks/drift-catalog.d.ts +0 -68
- package/dist/hooks/drift-catalog.d.ts.map +0 -1
- package/dist/hooks/drift-catalog.js +0 -184
- package/dist/hooks/drift-catalog.js.map +0 -1
- package/dist/hooks/drift-catalog.test.js +0 -154
- package/dist/hooks/drift-patterns.d.ts +0 -110
- package/dist/hooks/drift-patterns.d.ts.map +0 -1
- package/dist/hooks/drift-patterns.js +0 -289
- package/dist/hooks/drift-patterns.js.map +0 -1
- package/dist/hooks/drift-patterns.test.js +0 -325
- package/dist/hooks/engine-vocab-gate.d.ts +0 -108
- package/dist/hooks/engine-vocab-gate.d.ts.map +0 -1
- package/dist/hooks/engine-vocab-gate.js +0 -225
- package/dist/hooks/engine-vocab-gate.js.map +0 -1
- package/dist/hooks/engine-vocab-gate.test.js +0 -170
- package/dist/hooks/heartbeat.d.ts +0 -107
- package/dist/hooks/heartbeat.d.ts.map +0 -1
- package/dist/hooks/heartbeat.js +0 -316
- package/dist/hooks/heartbeat.js.map +0 -1
- package/dist/hooks/heartbeat.test.js +0 -393
- package/dist/hooks/honesty-ledger-session-scope.test.js +0 -100
- package/dist/hooks/honesty-ledger.d.ts +0 -123
- package/dist/hooks/honesty-ledger.d.ts.map +0 -1
- package/dist/hooks/honesty-ledger.js +0 -226
- package/dist/hooks/honesty-ledger.js.map +0 -1
- package/dist/hooks/honesty-ledger.test.js +0 -466
- package/dist/hooks/inline-report-check.d.ts +0 -63
- package/dist/hooks/inline-report-check.d.ts.map +0 -1
- package/dist/hooks/inline-report-check.js +0 -88
- package/dist/hooks/inline-report-check.js.map +0 -1
- package/dist/hooks/inline-report-check.test.js +0 -96
- package/dist/hooks/pre-tool-use.d.ts +0 -62
- package/dist/hooks/pre-tool-use.d.ts.map +0 -1
- package/dist/hooks/pre-tool-use.js +0 -342
- package/dist/hooks/pre-tool-use.js.map +0 -1
- package/dist/hooks/pre-tool-use.test.js +0 -134
- package/dist/hooks/session-end.d.ts +0 -15
- package/dist/hooks/session-end.d.ts.map +0 -1
- package/dist/hooks/session-end.js +0 -60
- package/dist/hooks/session-end.js.map +0 -1
- package/dist/hooks/session-end.test.js +0 -52
- package/dist/hooks/stop.d.ts +0 -35
- package/dist/hooks/stop.d.ts.map +0 -1
- package/dist/hooks/stop.js +0 -136
- package/dist/hooks/stop.js.map +0 -1
- package/dist/hooks/transcript-active-task.test.js +0 -342
- package/dist/hooks/transcript.d.ts +0 -26
- package/dist/hooks/transcript.d.ts.map +0 -1
- package/dist/hooks/transcript.js +0 -266
- package/dist/hooks/transcript.js.map +0 -1
- package/dist/hooks/transcript.test.js +0 -103
- package/dist/hooks/user-prompt-submit.d.ts +0 -74
- package/dist/hooks/user-prompt-submit.d.ts.map +0 -1
- package/dist/hooks/user-prompt-submit.js +0 -256
- package/dist/hooks/user-prompt-submit.js.map +0 -1
- package/dist/hooks/user-prompt-submit.test.js +0 -118
- package/dist/hooks/versioning-gate.d.ts +0 -101
- package/dist/hooks/versioning-gate.d.ts.map +0 -1
- package/dist/hooks/versioning-gate.js +0 -245
- package/dist/hooks/versioning-gate.js.map +0 -1
- package/dist/hooks/versioning-gate.test.js +0 -368
- package/dist/hooks/workflow-gate.d.ts +0 -64
- package/dist/hooks/workflow-gate.d.ts.map +0 -1
- package/dist/hooks/workflow-gate.js +0 -152
- package/dist/hooks/workflow-gate.js.map +0 -1
- package/dist/hooks/workflow-gate.test.js +0 -197
- package/dist/hooks-cli.d.ts +0 -25
- package/dist/hooks-cli.d.ts.map +0 -1
- package/dist/hooks-cli.js +0 -286
- package/dist/hooks-cli.js.map +0 -1
- package/dist/hooks-cli.test.js +0 -148
- package/dist/origin.d.ts +0 -16
- package/dist/origin.d.ts.map +0 -1
- package/dist/origin.js +0 -92
- package/dist/origin.js.map +0 -1
- package/dist/packs/seed_lessons_ingest.d.ts +0 -30
- package/dist/packs/seed_lessons_ingest.d.ts.map +0 -1
- package/dist/packs/seed_lessons_ingest.js +0 -107
- package/dist/packs/seed_lessons_ingest.js.map +0 -1
- package/dist/project-cli.d.ts +0 -7
- package/dist/project-cli.d.ts.map +0 -1
- package/dist/project-cli.js +0 -145
- package/dist/project-cli.js.map +0 -1
- package/dist/project.d.ts +0 -127
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js +0 -281
- package/dist/project.js.map +0 -1
- package/dist/project.test.js +0 -287
- package/dist/rag/backends/loop_engine.d.ts +0 -61
- package/dist/rag/backends/loop_engine.d.ts.map +0 -1
- package/dist/rag/backends/loop_engine.js +0 -160
- package/dist/rag/backends/loop_engine.js.map +0 -1
- package/dist/recall.d.ts +0 -82
- package/dist/recall.d.ts.map +0 -1
- package/dist/recall.js +0 -81
- package/dist/recall.js.map +0 -1
- package/dist/runtime/agent_bridge/autospawn.d.ts +0 -131
- package/dist/runtime/agent_bridge/autospawn.d.ts.map +0 -1
- package/dist/runtime/agent_bridge/autospawn.js +0 -251
- package/dist/runtime/agent_bridge/autospawn.js.map +0 -1
- package/dist/runtime/chain_state.d.ts +0 -124
- package/dist/runtime/chain_state.d.ts.map +0 -1
- package/dist/runtime/chain_state.js +0 -189
- package/dist/runtime/chain_state.js.map +0 -1
- package/dist/runtime/hooks/permission_decision.d.ts +0 -34
- package/dist/runtime/hooks/permission_decision.d.ts.map +0 -1
- package/dist/runtime/hooks/permission_decision.js +0 -39
- package/dist/runtime/hooks/permission_decision.js.map +0 -1
- package/dist/runtime/workflow_fsm.d.ts +0 -21
- package/dist/runtime/workflow_fsm.d.ts.map +0 -1
- package/dist/runtime/workflow_fsm.js +0 -25
- package/dist/runtime/workflow_fsm.js.map +0 -1
- package/dist/runtime/workflow_map.d.ts +0 -26
- package/dist/runtime/workflow_map.d.ts.map +0 -1
- package/dist/runtime/workflow_map.js +0 -38
- package/dist/runtime/workflow_map.js.map +0 -1
- package/dist/scope.d.ts +0 -48
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -111
- package/dist/scope.js.map +0 -1
- package/dist/setup/cli/topic_create_step.d.ts +0 -84
- package/dist/setup/cli/topic_create_step.d.ts.map +0 -1
- package/dist/setup/cli/topic_create_step.js +0 -213
- package/dist/setup/cli/topic_create_step.js.map +0 -1
- package/dist/system-export.d.ts +0 -65
- package/dist/system-export.d.ts.map +0 -1
- package/dist/system-export.js +0 -194
- package/dist/system-export.js.map +0 -1
- package/dist/utterance/classifier.d.ts +0 -53
- package/dist/utterance/classifier.d.ts.map +0 -1
- package/dist/utterance/classifier.js +0 -184
- package/dist/utterance/classifier.js.map +0 -1
- package/dist/utterance/classifier.test.js +0 -147
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for #124 — token-threshold heartbeat that replaces the auto-
|
|
3
|
-
* classifier subprocess.
|
|
4
|
-
*/
|
|
5
|
-
import * as crypto from "node:crypto";
|
|
6
|
-
import { promises as fs } from "node:fs";
|
|
7
|
-
import * as os from "node:os";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
10
|
-
import { DEFAULT_HEARTBEAT_TOKENS, checkAndMaybeArm, consumePendingHeartbeat, estimateTokens, estimateTranscriptTokens, formatHeartbeatNudge, heartbeatSessionFiles, heartbeatThresholdTokens, readCheckpoint, writeCheckpoint, } from "./heartbeat.js";
|
|
11
|
-
let tmpRoot;
|
|
12
|
-
const SESSION = "heartbeat-test";
|
|
13
|
-
beforeEach(async () => {
|
|
14
|
-
tmpRoot = path.join(os.tmpdir(), `oscli-heartbeat-${crypto.randomUUID()}`);
|
|
15
|
-
await fs.mkdir(tmpRoot, { recursive: true });
|
|
16
|
-
// Ensure no env override leaks across tests.
|
|
17
|
-
delete process.env.OPENSQUID_HEARTBEAT_TOKENS;
|
|
18
|
-
});
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
21
|
-
delete process.env.OPENSQUID_HEARTBEAT_TOKENS;
|
|
22
|
-
});
|
|
23
|
-
// ---------------------------------------------------------------------
|
|
24
|
-
// estimateTokens / heartbeatThresholdTokens
|
|
25
|
-
// ---------------------------------------------------------------------
|
|
26
|
-
describe("estimateTokens", () => {
|
|
27
|
-
it("returns 0 for empty / null-ish input", () => {
|
|
28
|
-
expect(estimateTokens("")).toBe(0);
|
|
29
|
-
});
|
|
30
|
-
it("approximates chars/4", () => {
|
|
31
|
-
expect(estimateTokens("aaaa")).toBe(1);
|
|
32
|
-
expect(estimateTokens("a".repeat(80))).toBe(20);
|
|
33
|
-
expect(estimateTokens("a".repeat(100))).toBe(25);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
describe("heartbeatThresholdTokens", () => {
|
|
37
|
-
it("returns default when env unset", () => {
|
|
38
|
-
expect(heartbeatThresholdTokens()).toBe(DEFAULT_HEARTBEAT_TOKENS);
|
|
39
|
-
});
|
|
40
|
-
it("honors OPENSQUID_HEARTBEAT_TOKENS positive integer", () => {
|
|
41
|
-
process.env.OPENSQUID_HEARTBEAT_TOKENS = "5000";
|
|
42
|
-
expect(heartbeatThresholdTokens()).toBe(5000);
|
|
43
|
-
});
|
|
44
|
-
it("falls back to default when env value is zero / negative / NaN", () => {
|
|
45
|
-
for (const bad of ["0", "-1", "abc", ""]) {
|
|
46
|
-
process.env.OPENSQUID_HEARTBEAT_TOKENS = bad;
|
|
47
|
-
expect(heartbeatThresholdTokens()).toBe(DEFAULT_HEARTBEAT_TOKENS);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
// ---------------------------------------------------------------------
|
|
52
|
-
// estimateTranscriptTokens
|
|
53
|
-
// ---------------------------------------------------------------------
|
|
54
|
-
describe("estimateTranscriptTokens (0.7.7 #161)", () => {
|
|
55
|
-
it("returns 0 when transcript file is missing", async () => {
|
|
56
|
-
const r = await estimateTranscriptTokens(path.join(tmpRoot, "nope.jsonl"));
|
|
57
|
-
expect(r).toBe(0);
|
|
58
|
-
});
|
|
59
|
-
it("returns 0 for an empty file", async () => {
|
|
60
|
-
const p = path.join(tmpRoot, "empty.jsonl");
|
|
61
|
-
await fs.writeFile(p, "");
|
|
62
|
-
expect(await estimateTranscriptTokens(p)).toBe(0);
|
|
63
|
-
});
|
|
64
|
-
it("counts user.string content", async () => {
|
|
65
|
-
const p = path.join(tmpRoot, "transcript.jsonl");
|
|
66
|
-
const line = JSON.stringify({
|
|
67
|
-
type: "user",
|
|
68
|
-
message: { role: "user", content: "x".repeat(400) },
|
|
69
|
-
});
|
|
70
|
-
await fs.writeFile(p, line + "\n");
|
|
71
|
-
// 400 chars / 4 = 100 tokens
|
|
72
|
-
expect(await estimateTranscriptTokens(p)).toBe(100);
|
|
73
|
-
});
|
|
74
|
-
it("counts assistant text blocks", async () => {
|
|
75
|
-
const p = path.join(tmpRoot, "transcript.jsonl");
|
|
76
|
-
const line = JSON.stringify({
|
|
77
|
-
type: "assistant",
|
|
78
|
-
message: {
|
|
79
|
-
role: "assistant",
|
|
80
|
-
content: [
|
|
81
|
-
{ type: "text", text: "a".repeat(400) },
|
|
82
|
-
{ type: "text", text: "b".repeat(400) },
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
await fs.writeFile(p, line + "\n");
|
|
87
|
-
// 800 chars / 4 = 200 tokens
|
|
88
|
-
expect(await estimateTranscriptTokens(p)).toBe(200);
|
|
89
|
-
});
|
|
90
|
-
it("SKIPS thinking blocks (agent internal CoT)", async () => {
|
|
91
|
-
const p = path.join(tmpRoot, "transcript.jsonl");
|
|
92
|
-
const line = JSON.stringify({
|
|
93
|
-
type: "assistant",
|
|
94
|
-
message: {
|
|
95
|
-
content: [
|
|
96
|
-
{ type: "thinking", thinking: "x".repeat(10000), signature: "sig" },
|
|
97
|
-
{ type: "text", text: "hello" },
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
await fs.writeFile(p, line + "\n");
|
|
102
|
-
// Only "hello" (5 chars) counted → 2 tokens (ceiling)
|
|
103
|
-
expect(await estimateTranscriptTokens(p)).toBe(2);
|
|
104
|
-
});
|
|
105
|
-
it("SKIPS tool_use blocks (compact + outbound work)", async () => {
|
|
106
|
-
const p = path.join(tmpRoot, "transcript.jsonl");
|
|
107
|
-
const line = JSON.stringify({
|
|
108
|
-
type: "assistant",
|
|
109
|
-
message: {
|
|
110
|
-
content: [
|
|
111
|
-
{ type: "tool_use", id: "x", name: "Bash", input: { command: "ls" } },
|
|
112
|
-
{ type: "text", text: "after the tool" },
|
|
113
|
-
],
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
await fs.writeFile(p, line + "\n");
|
|
117
|
-
// Only the text block (14 chars) → 4 tokens
|
|
118
|
-
expect(await estimateTranscriptTokens(p)).toBe(4);
|
|
119
|
-
});
|
|
120
|
-
it("CAPS tool_result content at 2000 chars (prevents tool-result inflation)", async () => {
|
|
121
|
-
const p = path.join(tmpRoot, "transcript.jsonl");
|
|
122
|
-
const line = JSON.stringify({
|
|
123
|
-
type: "user",
|
|
124
|
-
message: {
|
|
125
|
-
role: "user",
|
|
126
|
-
content: [
|
|
127
|
-
{
|
|
128
|
-
type: "tool_result",
|
|
129
|
-
tool_use_id: "x",
|
|
130
|
-
content: "z".repeat(50000), // huge file read
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
await fs.writeFile(p, line + "\n");
|
|
136
|
-
// Capped at 2000 chars / 4 = 500 tokens (NOT 12,500)
|
|
137
|
-
expect(await estimateTranscriptTokens(p)).toBe(500);
|
|
138
|
-
});
|
|
139
|
-
it("counts tool_result content array form (nested blocks)", async () => {
|
|
140
|
-
const p = path.join(tmpRoot, "transcript.jsonl");
|
|
141
|
-
const line = JSON.stringify({
|
|
142
|
-
type: "user",
|
|
143
|
-
message: {
|
|
144
|
-
content: [
|
|
145
|
-
{
|
|
146
|
-
type: "tool_result",
|
|
147
|
-
tool_use_id: "x",
|
|
148
|
-
content: [
|
|
149
|
-
{ type: "text", text: "y".repeat(800) },
|
|
150
|
-
{ type: "text", text: "y".repeat(800) },
|
|
151
|
-
],
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
await fs.writeFile(p, line + "\n");
|
|
157
|
-
// 1600 chars total (under 2000 cap) → 400 tokens
|
|
158
|
-
expect(await estimateTranscriptTokens(p)).toBe(400);
|
|
159
|
-
});
|
|
160
|
-
it("SKIPS non-conversation line types (system / permission-mode / file-history-snapshot / etc)", async () => {
|
|
161
|
-
const p = path.join(tmpRoot, "transcript.jsonl");
|
|
162
|
-
const lines = [
|
|
163
|
-
JSON.stringify({ type: "permission-mode", permissionMode: "default" }),
|
|
164
|
-
JSON.stringify({ type: "system", text: "x".repeat(10000) }),
|
|
165
|
-
JSON.stringify({ type: "file-history-snapshot", snapshot: {} }),
|
|
166
|
-
JSON.stringify({ type: "attachment", message: { content: "x".repeat(10000) } }),
|
|
167
|
-
JSON.stringify({ type: "ai-title", title: "Hello" }),
|
|
168
|
-
JSON.stringify({ type: "last-prompt", prompt: "x".repeat(10000) }),
|
|
169
|
-
].join("\n");
|
|
170
|
-
await fs.writeFile(p, lines);
|
|
171
|
-
expect(await estimateTranscriptTokens(p)).toBe(0);
|
|
172
|
-
});
|
|
173
|
-
it("tolerates malformed JSON lines (skips them)", async () => {
|
|
174
|
-
const p = path.join(tmpRoot, "transcript.jsonl");
|
|
175
|
-
const lines = [
|
|
176
|
-
"not json at all",
|
|
177
|
-
JSON.stringify({ type: "user", message: { content: "hello" } }),
|
|
178
|
-
"{partial json",
|
|
179
|
-
].join("\n");
|
|
180
|
-
await fs.writeFile(p, lines);
|
|
181
|
-
// Only "hello" counted (5 chars) → 2 tokens (ceiling)
|
|
182
|
-
expect(await estimateTranscriptTokens(p)).toBe(2);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
// ---------------------------------------------------------------------
|
|
186
|
-
// formatHeartbeatNudge
|
|
187
|
-
// ---------------------------------------------------------------------
|
|
188
|
-
describe("formatHeartbeatNudge", () => {
|
|
189
|
-
it("includes the delta + threshold + the recall instruction", () => {
|
|
190
|
-
const nudge = formatHeartbeatNudge(20000, 20000);
|
|
191
|
-
expect(nudge).toContain("20,000");
|
|
192
|
-
expect(nudge).toContain("recall");
|
|
193
|
-
expect(nudge).toContain("memorize");
|
|
194
|
-
expect(nudge).toContain("🦑");
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
// ---------------------------------------------------------------------
|
|
198
|
-
// Checkpoint IO
|
|
199
|
-
// ---------------------------------------------------------------------
|
|
200
|
-
describe("checkpoint IO", () => {
|
|
201
|
-
it("returns null when no checkpoint file exists", async () => {
|
|
202
|
-
expect(await readCheckpoint(SESSION, { dataRoot: tmpRoot })).toBeNull();
|
|
203
|
-
});
|
|
204
|
-
it("round-trips via writeCheckpoint", async () => {
|
|
205
|
-
await writeCheckpoint(SESSION, { last_token_count: 12345, last_checkpoint_at: "2026-05-15T00:00:00Z" }, { dataRoot: tmpRoot });
|
|
206
|
-
const back = await readCheckpoint(SESSION, { dataRoot: tmpRoot });
|
|
207
|
-
expect(back?.last_token_count).toBe(12345);
|
|
208
|
-
expect(back?.last_checkpoint_at).toBe("2026-05-15T00:00:00Z");
|
|
209
|
-
});
|
|
210
|
-
it("returns null on malformed JSON", async () => {
|
|
211
|
-
const p = path.join(tmpRoot, "sessions", SESSION);
|
|
212
|
-
await fs.mkdir(p, { recursive: true });
|
|
213
|
-
await fs.writeFile(path.join(p, "heartbeat-checkpoint.json"), "not json");
|
|
214
|
-
expect(await readCheckpoint(SESSION, { dataRoot: tmpRoot })).toBeNull();
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
// ---------------------------------------------------------------------
|
|
218
|
-
// checkAndMaybeArm — Stop hook entrypoint
|
|
219
|
-
// ---------------------------------------------------------------------
|
|
220
|
-
describe("checkAndMaybeArm", () => {
|
|
221
|
-
// 0.7.7 (#161): estimator now counts only user/assistant message bodies
|
|
222
|
-
// from valid JSONL lines, not raw file bytes. Helper writes a synthetic
|
|
223
|
-
// user message whose content has the requested char-count so existing
|
|
224
|
-
// crossing-math tests still work without reading a real transcript.
|
|
225
|
-
async function writeTranscript(chars) {
|
|
226
|
-
const p = path.join(tmpRoot, "transcript.jsonl");
|
|
227
|
-
const line = JSON.stringify({
|
|
228
|
-
type: "user",
|
|
229
|
-
message: { role: "user", content: "x".repeat(chars) },
|
|
230
|
-
});
|
|
231
|
-
await fs.writeFile(p, line + "\n");
|
|
232
|
-
return p;
|
|
233
|
-
}
|
|
234
|
-
it("returns null when transcript is missing / empty", async () => {
|
|
235
|
-
const r = await checkAndMaybeArm(SESSION, path.join(tmpRoot, "missing.jsonl"), {
|
|
236
|
-
dataRoot: tmpRoot,
|
|
237
|
-
});
|
|
238
|
-
expect(r).toBeNull();
|
|
239
|
-
});
|
|
240
|
-
it("arms a heartbeat on first crossing (no prior checkpoint)", async () => {
|
|
241
|
-
// 80000 chars -> 20000 tokens -> exactly threshold
|
|
242
|
-
const tpath = await writeTranscript(80000);
|
|
243
|
-
const nudge = await checkAndMaybeArm(SESSION, tpath, {
|
|
244
|
-
dataRoot: tmpRoot,
|
|
245
|
-
thresholdTokens: 20000,
|
|
246
|
-
});
|
|
247
|
-
expect(nudge).not.toBeNull();
|
|
248
|
-
expect(nudge).toContain("20,000");
|
|
249
|
-
// Checkpoint bumped to the current count.
|
|
250
|
-
const cp = await readCheckpoint(SESSION, { dataRoot: tmpRoot });
|
|
251
|
-
expect(cp?.last_token_count).toBe(20000);
|
|
252
|
-
});
|
|
253
|
-
it("does NOT arm again until threshold crossed from the new checkpoint", async () => {
|
|
254
|
-
// First crossing.
|
|
255
|
-
let tpath = await writeTranscript(80000);
|
|
256
|
-
expect(await checkAndMaybeArm(SESSION, tpath, { dataRoot: tmpRoot, thresholdTokens: 20000 })).not.toBeNull();
|
|
257
|
-
// Drain the previous nudge so we can detect a fresh one (or its absence).
|
|
258
|
-
await consumePendingHeartbeat(SESSION, { dataRoot: tmpRoot });
|
|
259
|
-
// Transcript grows by less than threshold from the checkpoint.
|
|
260
|
-
tpath = await writeTranscript(80000 + 4000); // +1000 tokens
|
|
261
|
-
const second = await checkAndMaybeArm(SESSION, tpath, {
|
|
262
|
-
dataRoot: tmpRoot,
|
|
263
|
-
thresholdTokens: 20000,
|
|
264
|
-
});
|
|
265
|
-
expect(second).toBeNull();
|
|
266
|
-
// Checkpoint stays at the first crossing.
|
|
267
|
-
const cp = await readCheckpoint(SESSION, { dataRoot: tmpRoot });
|
|
268
|
-
expect(cp?.last_token_count).toBe(20000);
|
|
269
|
-
});
|
|
270
|
-
it("resets stale baseline when checkpoint > 10x current (post-0.7.7 estimator migration)", async () => {
|
|
271
|
-
// Simulate a checkpoint left by the old estimator: 31M tokens for a
|
|
272
|
-
// 1.5M-token-real transcript. New estimator returns ~1.5M, baseline
|
|
273
|
-
// says 31M → naive delta is negative → would never fire. Reset
|
|
274
|
-
// logic must zero the baseline so the next crossing arms.
|
|
275
|
-
await writeCheckpoint(SESSION, { last_token_count: 31_000_000, last_checkpoint_at: "2026-05-17T00:00:00Z" }, { dataRoot: tmpRoot });
|
|
276
|
-
const tpath = await writeTranscript(80000); // 20K tokens
|
|
277
|
-
const nudge = await checkAndMaybeArm(SESSION, tpath, {
|
|
278
|
-
dataRoot: tmpRoot,
|
|
279
|
-
thresholdTokens: 20000,
|
|
280
|
-
});
|
|
281
|
-
expect(nudge).not.toBeNull();
|
|
282
|
-
const cp = await readCheckpoint(SESSION, { dataRoot: tmpRoot });
|
|
283
|
-
expect(cp?.last_token_count).toBe(20000);
|
|
284
|
-
});
|
|
285
|
-
it("does NOT reset baseline when checkpoint is within reasonable range", async () => {
|
|
286
|
-
// Baseline only 2x current — not stale, just slow growth (or
|
|
287
|
-
// transcript shrunk via compaction). Don't reset.
|
|
288
|
-
await writeCheckpoint(SESSION, { last_token_count: 40000, last_checkpoint_at: "2026-05-17T00:00:00Z" }, { dataRoot: tmpRoot });
|
|
289
|
-
const tpath = await writeTranscript(80000); // 20K tokens, baseline 40K, delta = -20K
|
|
290
|
-
const nudge = await checkAndMaybeArm(SESSION, tpath, {
|
|
291
|
-
dataRoot: tmpRoot,
|
|
292
|
-
thresholdTokens: 20000,
|
|
293
|
-
});
|
|
294
|
-
expect(nudge).toBeNull(); // negative delta, but no reset → no fire
|
|
295
|
-
});
|
|
296
|
-
it("arms again on each subsequent threshold crossing", async () => {
|
|
297
|
-
// First crossing at 20K tokens.
|
|
298
|
-
let tpath = await writeTranscript(80000);
|
|
299
|
-
expect(await checkAndMaybeArm(SESSION, tpath, { dataRoot: tmpRoot, thresholdTokens: 20000 })).not.toBeNull();
|
|
300
|
-
await consumePendingHeartbeat(SESSION, { dataRoot: tmpRoot });
|
|
301
|
-
// Second crossing at 40K tokens.
|
|
302
|
-
tpath = await writeTranscript(160000);
|
|
303
|
-
expect(await checkAndMaybeArm(SESSION, tpath, { dataRoot: tmpRoot, thresholdTokens: 20000 })).not.toBeNull();
|
|
304
|
-
const cp = await readCheckpoint(SESSION, { dataRoot: tmpRoot });
|
|
305
|
-
expect(cp?.last_token_count).toBe(40000);
|
|
306
|
-
});
|
|
307
|
-
it("does not arm when below threshold and no prior checkpoint", async () => {
|
|
308
|
-
const tpath = await writeTranscript(40000); // 10K tokens < 20K threshold
|
|
309
|
-
const r = await checkAndMaybeArm(SESSION, tpath, {
|
|
310
|
-
dataRoot: tmpRoot,
|
|
311
|
-
thresholdTokens: 20000,
|
|
312
|
-
});
|
|
313
|
-
expect(r).toBeNull();
|
|
314
|
-
// No checkpoint written when we didn't arm.
|
|
315
|
-
expect(await readCheckpoint(SESSION, { dataRoot: tmpRoot })).toBeNull();
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
// ---------------------------------------------------------------------
|
|
319
|
-
// consumePendingHeartbeat — UserPromptSubmit hook entrypoint
|
|
320
|
-
// ---------------------------------------------------------------------
|
|
321
|
-
describe("consumePendingHeartbeat", () => {
|
|
322
|
-
it("returns null when no pending marker", async () => {
|
|
323
|
-
expect(await consumePendingHeartbeat(SESSION, { dataRoot: tmpRoot })).toBeNull();
|
|
324
|
-
});
|
|
325
|
-
it("returns the armed nudge and removes the marker (one-shot)", async () => {
|
|
326
|
-
// Arm. 0.7.7 (#161): estimator now requires valid JSONL; wrap the
|
|
327
|
-
// body content so the line parses as a user message.
|
|
328
|
-
const tpath = path.join(tmpRoot, "transcript.jsonl");
|
|
329
|
-
const line = JSON.stringify({ type: "user", message: { content: "x".repeat(80000) } });
|
|
330
|
-
await fs.writeFile(tpath, line + "\n");
|
|
331
|
-
await checkAndMaybeArm(SESSION, tpath, { dataRoot: tmpRoot, thresholdTokens: 20000 });
|
|
332
|
-
const first = await consumePendingHeartbeat(SESSION, { dataRoot: tmpRoot });
|
|
333
|
-
expect(first).not.toBeNull();
|
|
334
|
-
expect(first).toContain("🦑");
|
|
335
|
-
// Second consume returns null — marker was deleted.
|
|
336
|
-
const second = await consumePendingHeartbeat(SESSION, { dataRoot: tmpRoot });
|
|
337
|
-
expect(second).toBeNull();
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
// ---------------------------------------------------------------------
|
|
341
|
-
// SessionEnd cleanup hook surface
|
|
342
|
-
// ---------------------------------------------------------------------
|
|
343
|
-
describe("heartbeatSessionFiles", () => {
|
|
344
|
-
it("returns the two paths SessionEnd should remove", () => {
|
|
345
|
-
const files = heartbeatSessionFiles(SESSION, tmpRoot);
|
|
346
|
-
expect(files.some((p) => p.endsWith("heartbeat-checkpoint.json"))).toBe(true);
|
|
347
|
-
expect(files.some((p) => p.endsWith("heartbeat-pending.txt"))).toBe(true);
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
// =====================================================================
|
|
351
|
-
// 0.7.26 / D7 — recall-required flag (heartbeat → block until recall)
|
|
352
|
-
// =====================================================================
|
|
353
|
-
describe("recall-required flag (D7)", () => {
|
|
354
|
-
let tmp;
|
|
355
|
-
beforeEach(async () => {
|
|
356
|
-
tmp = path.join(os.tmpdir(), `opensquid-recall-flag-${crypto.randomUUID()}`);
|
|
357
|
-
await fs.mkdir(tmp, { recursive: true });
|
|
358
|
-
});
|
|
359
|
-
afterEach(async () => {
|
|
360
|
-
await fs.rm(tmp, { recursive: true, force: true });
|
|
361
|
-
});
|
|
362
|
-
it("isRecallRequired returns false when flag was never set", async () => {
|
|
363
|
-
const { isRecallRequired } = await import("./heartbeat.js");
|
|
364
|
-
expect(await isRecallRequired("sess-1", { dataRoot: tmp })).toBe(false);
|
|
365
|
-
});
|
|
366
|
-
it("markRecallRequired creates the flag; isRecallRequired returns true", async () => {
|
|
367
|
-
const { markRecallRequired, isRecallRequired } = await import("./heartbeat.js");
|
|
368
|
-
await markRecallRequired("sess-2", { dataRoot: tmp });
|
|
369
|
-
expect(await isRecallRequired("sess-2", { dataRoot: tmp })).toBe(true);
|
|
370
|
-
});
|
|
371
|
-
it("clearRecallRequired removes the flag", async () => {
|
|
372
|
-
const { markRecallRequired, clearRecallRequired, isRecallRequired } = await import("./heartbeat.js");
|
|
373
|
-
await markRecallRequired("sess-3", { dataRoot: tmp });
|
|
374
|
-
expect(await isRecallRequired("sess-3", { dataRoot: tmp })).toBe(true);
|
|
375
|
-
await clearRecallRequired("sess-3", { dataRoot: tmp });
|
|
376
|
-
expect(await isRecallRequired("sess-3", { dataRoot: tmp })).toBe(false);
|
|
377
|
-
});
|
|
378
|
-
it("clearRecallRequired is idempotent (clear without prior mark is fine)", async () => {
|
|
379
|
-
const { clearRecallRequired } = await import("./heartbeat.js");
|
|
380
|
-
await expect(clearRecallRequired("sess-never", { dataRoot: tmp })).resolves.toBeUndefined();
|
|
381
|
-
});
|
|
382
|
-
it("flags are per-session — setting one session doesn't affect another", async () => {
|
|
383
|
-
const { markRecallRequired, isRecallRequired } = await import("./heartbeat.js");
|
|
384
|
-
await markRecallRequired("sess-A", { dataRoot: tmp });
|
|
385
|
-
expect(await isRecallRequired("sess-A", { dataRoot: tmp })).toBe(true);
|
|
386
|
-
expect(await isRecallRequired("sess-B", { dataRoot: tmp })).toBe(false);
|
|
387
|
-
});
|
|
388
|
-
it("heartbeatSessionFiles includes the recall-required flag path for SessionEnd cleanup", async () => {
|
|
389
|
-
const { heartbeatSessionFiles } = await import("./heartbeat.js");
|
|
390
|
-
const files = heartbeatSessionFiles("sess-1", tmp);
|
|
391
|
-
expect(files.some((p) => p.endsWith("recall-required.flag"))).toBe(true);
|
|
392
|
-
});
|
|
393
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests specifically for the session-scope fix (#114).
|
|
3
|
-
*
|
|
4
|
-
* Verifies that:
|
|
5
|
-
* 1. The ledger accumulates across multiple turns within a session.
|
|
6
|
-
* 2. A claim made in turn N is satisfied by evidence from turn 1.
|
|
7
|
-
* 3. clearSession wipes both ledger + broken-promises.
|
|
8
|
-
* 4. Stop hook's de-dupe behavior — re-running reconcile on the same
|
|
9
|
-
* text doesn't double-record the broken promise.
|
|
10
|
-
*/
|
|
11
|
-
import * as crypto from "node:crypto";
|
|
12
|
-
import { promises as fs } from "node:fs";
|
|
13
|
-
import * as os from "node:os";
|
|
14
|
-
import * as path from "node:path";
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
16
|
-
import { clearSession, clearTurnLedger, reconcile, readBrokenPromises, readTurnLedger, recordBrokenPromise, recordToolCall, } from "./honesty-ledger.js";
|
|
17
|
-
let tmpRoot;
|
|
18
|
-
const SESSION = "scope-test-session";
|
|
19
|
-
beforeEach(async () => {
|
|
20
|
-
tmpRoot = path.join(os.tmpdir(), `oscli-honesty-scope-${crypto.randomUUID()}`);
|
|
21
|
-
await fs.mkdir(tmpRoot, { recursive: true });
|
|
22
|
-
});
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
25
|
-
});
|
|
26
|
-
describe("session-scoped ledger (#114 fix)", () => {
|
|
27
|
-
it("accumulates tool calls across multiple turns", async () => {
|
|
28
|
-
// Turn 1: ran npm test
|
|
29
|
-
await recordToolCall(SESSION, "Bash", "npm test", { dataRoot: tmpRoot });
|
|
30
|
-
// Turn 2: ran cargo check
|
|
31
|
-
await recordToolCall(SESSION, "Bash", "cargo check", { dataRoot: tmpRoot });
|
|
32
|
-
// Turn 3: read a file
|
|
33
|
-
await recordToolCall(SESSION, "Read", "/x.ts", { dataRoot: tmpRoot });
|
|
34
|
-
const ledger = await readTurnLedger(SESSION, { dataRoot: tmpRoot });
|
|
35
|
-
expect(ledger).toHaveLength(3);
|
|
36
|
-
expect(ledger.map((e) => e.tool)).toEqual(["Bash", "Bash", "Read"]);
|
|
37
|
-
});
|
|
38
|
-
it("recap text in turn N is satisfied by tool call from turn 1 (THE FIX)", async () => {
|
|
39
|
-
// Turn 1: actually ran tests
|
|
40
|
-
await recordToolCall(SESSION, "Bash", "npm test", { dataRoot: tmpRoot });
|
|
41
|
-
// Turn 5 (much later): assistant says "tests pass" in recap text
|
|
42
|
-
const recapText = "Here's a summary. Tests pass and build is green.";
|
|
43
|
-
const ledger = await readTurnLedger(SESSION, { dataRoot: tmpRoot });
|
|
44
|
-
const broken = reconcile(recapText, ledger);
|
|
45
|
-
// Before the fix: would flag "tests pass" as a broken promise.
|
|
46
|
-
// After the fix: ledger has the prior turn's npm test, so satisfied.
|
|
47
|
-
expect(broken.map((b) => b.claim_id)).not.toContain("running-tests");
|
|
48
|
-
});
|
|
49
|
-
it("genuinely lying recap is still caught (no false-negative regression)", async () => {
|
|
50
|
-
// Turn 1: read a file
|
|
51
|
-
await recordToolCall(SESSION, "Read", "/foo.ts", { dataRoot: tmpRoot });
|
|
52
|
-
// Turn 2: claim "tests pass" but NO test was ever run in this session
|
|
53
|
-
const ledger = await readTurnLedger(SESSION, { dataRoot: tmpRoot });
|
|
54
|
-
const broken = reconcile("Tests pass.", ledger);
|
|
55
|
-
expect(broken.map((b) => b.claim_id)).toContain("running-tests");
|
|
56
|
-
});
|
|
57
|
-
it("clearTurnLedger removes only the ledger file (not broken-promises)", async () => {
|
|
58
|
-
await recordToolCall(SESSION, "Bash", "ls", { dataRoot: tmpRoot });
|
|
59
|
-
await recordBrokenPromise(SESSION, {
|
|
60
|
-
ts: "t",
|
|
61
|
-
claim_id: "fake",
|
|
62
|
-
claim_label: "fake",
|
|
63
|
-
matched_text: "fake",
|
|
64
|
-
reason: "fake",
|
|
65
|
-
}, { dataRoot: tmpRoot });
|
|
66
|
-
await clearTurnLedger(SESSION, { dataRoot: tmpRoot });
|
|
67
|
-
expect(await readTurnLedger(SESSION, { dataRoot: tmpRoot })).toEqual([]);
|
|
68
|
-
// broken-promises survives a turn-ledger clear
|
|
69
|
-
expect(await readBrokenPromises(SESSION, { dataRoot: tmpRoot })).toHaveLength(1);
|
|
70
|
-
});
|
|
71
|
-
it("clearSession wipes BOTH ledger and broken-promises", async () => {
|
|
72
|
-
await recordToolCall(SESSION, "Bash", "ls", { dataRoot: tmpRoot });
|
|
73
|
-
await recordBrokenPromise(SESSION, {
|
|
74
|
-
ts: "t",
|
|
75
|
-
claim_id: "fake",
|
|
76
|
-
claim_label: "fake",
|
|
77
|
-
matched_text: "fake",
|
|
78
|
-
reason: "fake",
|
|
79
|
-
}, { dataRoot: tmpRoot });
|
|
80
|
-
await clearSession(SESSION, { dataRoot: tmpRoot });
|
|
81
|
-
expect(await readTurnLedger(SESSION, { dataRoot: tmpRoot })).toEqual([]);
|
|
82
|
-
expect(await readBrokenPromises(SESSION, { dataRoot: tmpRoot })).toEqual([]);
|
|
83
|
-
});
|
|
84
|
-
it("clearSession is idempotent (no throw on already-clean session)", async () => {
|
|
85
|
-
await clearSession(SESSION, { dataRoot: tmpRoot });
|
|
86
|
-
await clearSession(SESSION, { dataRoot: tmpRoot });
|
|
87
|
-
// No throw.
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
describe("stuck-broken-promise dedupe (#114 fix companion)", () => {
|
|
91
|
-
it("reconcile on the same text twice returns the same set", async () => {
|
|
92
|
-
// No tool calls. Claim text triggers two patterns.
|
|
93
|
-
const broken1 = reconcile("Tests pass and committed.", []);
|
|
94
|
-
const broken2 = reconcile("Tests pass and committed.", []);
|
|
95
|
-
expect(broken1.map((b) => b.claim_id).sort()).toEqual(broken2.map((b) => b.claim_id).sort());
|
|
96
|
-
// Both should flag running-tests + committed
|
|
97
|
-
expect(broken1.map((b) => b.claim_id)).toContain("running-tests");
|
|
98
|
-
expect(broken1.map((b) => b.claim_id)).toContain("committed");
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Honesty ledger — catches claim-vs-action gaps in assistant turns.
|
|
3
|
-
*
|
|
4
|
-
* The agent makes claims like "running tests now" or "starting research"
|
|
5
|
-
* or "committed" in its text output. These claims are checkable against
|
|
6
|
-
* the tool calls that actually happened in the same turn. When the
|
|
7
|
-
* claim has no matching tool call, that's a "broken promise" — opensquid
|
|
8
|
-
* records it so the next turn surfaces it to the agent for correction.
|
|
9
|
-
*
|
|
10
|
-
* Storage:
|
|
11
|
-
* <data-root>/sessions/<session-id>/turn-ledger.jsonl
|
|
12
|
-
* One JSON line per tool call this turn, appended by the PreToolUse
|
|
13
|
-
* hook. Cleared at turn-end after reconciliation.
|
|
14
|
-
*
|
|
15
|
-
* <data-root>/sessions/<session-id>/broken-promises.jsonl
|
|
16
|
-
* Append-only ledger of claims that lacked matching evidence.
|
|
17
|
-
* Surfaced to the agent on the NEXT turn via SessionStart/
|
|
18
|
-
* UserPromptSubmit hook output.
|
|
19
|
-
*/
|
|
20
|
-
export type ClaimEvidenceShape = {
|
|
21
|
-
kind: "any_tool";
|
|
22
|
-
} | {
|
|
23
|
-
kind: "bash_contains";
|
|
24
|
-
needle: string;
|
|
25
|
-
} | {
|
|
26
|
-
kind: "bash_regex";
|
|
27
|
-
pattern: string;
|
|
28
|
-
} | {
|
|
29
|
-
kind: "tool_called";
|
|
30
|
-
tool: string;
|
|
31
|
-
} | {
|
|
32
|
-
kind: "any_of";
|
|
33
|
-
options: ClaimEvidenceShape[];
|
|
34
|
-
} | {
|
|
35
|
-
kind: "input_contains";
|
|
36
|
-
tool: string;
|
|
37
|
-
needle: string;
|
|
38
|
-
};
|
|
39
|
-
export interface ClaimPattern {
|
|
40
|
-
/** Stable id (e.g. "research-start"). */
|
|
41
|
-
id: string;
|
|
42
|
-
/** Regex matched against assistant text. */
|
|
43
|
-
text_regex: string;
|
|
44
|
-
/** What proof of action satisfies this claim. */
|
|
45
|
-
evidence: ClaimEvidenceShape;
|
|
46
|
-
/** Short label surfaced to the agent when the promise is broken. */
|
|
47
|
-
promise_label: string;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Claim catalog — sourced from the bundled-default codex (0.7.17,
|
|
51
|
-
* drift-as-codex chunk 3b). Previously a hand-maintained TS array;
|
|
52
|
-
* now loaded once at module init from
|
|
53
|
-
* `src/codex/bundled-default/codex.yaml` via the chunk-2 loader.
|
|
54
|
-
*
|
|
55
|
-
* Fail-open: if the codex is unloadable, the catalog is empty and
|
|
56
|
-
* no claims fire. Better silent under-enforcement than a hook crash.
|
|
57
|
-
*/
|
|
58
|
-
export declare const CLAIM_PATTERNS: ClaimPattern[];
|
|
59
|
-
/**
|
|
60
|
-
* One tool-call entry, persisted across ALL turns in the same Claude
|
|
61
|
-
* Code session. Cleared only at session end (or explicit
|
|
62
|
-
* `clearSessionLedger`).
|
|
63
|
-
*
|
|
64
|
-
* #114 (2026-05-15) — v0.4.C.1 fix: previously this was a PER-TURN
|
|
65
|
-
* ledger that got cleared by the Stop hook, which caused recap text
|
|
66
|
-
* describing prior-turn work to be flagged as broken promises. Now
|
|
67
|
-
* the ledger accumulates across the whole session so claims like
|
|
68
|
-
* "tests pass" satisfy against any `npm test` from any earlier turn
|
|
69
|
-
* in the same session.
|
|
70
|
-
*/
|
|
71
|
-
export interface TurnLedgerEntry {
|
|
72
|
-
ts: string;
|
|
73
|
-
tool: string;
|
|
74
|
-
/** Subset of tool_input relevant to reconciliation. */
|
|
75
|
-
input_summary: string;
|
|
76
|
-
}
|
|
77
|
-
/** Called by PreToolUse hook to record what the agent is about to do. */
|
|
78
|
-
export declare function recordToolCall(sessionId: string, tool: string, inputSummary: string, options?: {
|
|
79
|
-
dataRoot?: string;
|
|
80
|
-
}): Promise<void>;
|
|
81
|
-
/** Read the session-scoped ledger (every tool call so far this session). */
|
|
82
|
-
export declare function readTurnLedger(sessionId: string, options?: {
|
|
83
|
-
dataRoot?: string;
|
|
84
|
-
}): Promise<TurnLedgerEntry[]>;
|
|
85
|
-
/**
|
|
86
|
-
* Clear the session ledger. ONLY called at session end (or by tests).
|
|
87
|
-
* Stop hook does NOT call this anymore — the ledger persists across
|
|
88
|
-
* turns to avoid recap-text false-positives.
|
|
89
|
-
*/
|
|
90
|
-
export declare function clearTurnLedger(sessionId: string, options?: {
|
|
91
|
-
dataRoot?: string;
|
|
92
|
-
}): Promise<void>;
|
|
93
|
-
/**
|
|
94
|
-
* Explicit session-end clear: wipes everything opensquid wrote under
|
|
95
|
-
* this session's directory — turn ledger, broken promises, plus the
|
|
96
|
-
* heartbeat checkpoint and pending marker (#124). Files are removed
|
|
97
|
-
* individually so unrelated files in the session dir survive (in case
|
|
98
|
-
* a future hook drops something else there).
|
|
99
|
-
*/
|
|
100
|
-
export declare function clearSession(sessionId: string, options?: {
|
|
101
|
-
dataRoot?: string;
|
|
102
|
-
}): Promise<void>;
|
|
103
|
-
export interface BrokenPromise {
|
|
104
|
-
ts: string;
|
|
105
|
-
claim_id: string;
|
|
106
|
-
claim_label: string;
|
|
107
|
-
matched_text: string;
|
|
108
|
-
reason: string;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Scan assistant text for claim phrases, reconcile against the ledger,
|
|
112
|
-
* return any unfulfilled promises.
|
|
113
|
-
*/
|
|
114
|
-
export declare function reconcile(assistantText: string, ledger: TurnLedgerEntry[]): BrokenPromise[];
|
|
115
|
-
/** Append a broken promise to the session's append-only ledger. */
|
|
116
|
-
export declare function recordBrokenPromise(sessionId: string, promise: BrokenPromise, options?: {
|
|
117
|
-
dataRoot?: string;
|
|
118
|
-
}): Promise<void>;
|
|
119
|
-
/** Read all broken promises for a session (used by next turn's hook). */
|
|
120
|
-
export declare function readBrokenPromises(sessionId: string, options?: {
|
|
121
|
-
dataRoot?: string;
|
|
122
|
-
}): Promise<BrokenPromise[]>;
|
|
123
|
-
//# sourceMappingURL=honesty-ledger.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"honesty-ledger.d.ts","sourceRoot":"","sources":["../../src.legacy/hooks/honesty-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAaH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAKrC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,kBAAkB,EAAE,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;CACvB;AA4CD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,EAAE,YAAY,EAUrC,CAAC;AAML;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAC;CACvB;AAgBD,yEAAyE;AACzE,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,4EAA4E;AAC5E,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,eAAe,EAAE,CAAC,CAU5B;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CAcf;AAMD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE,CAqB3F;AA0BD,mEAAmE;AACnE,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,yEAAyE;AACzE,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,aAAa,EAAE,CAAC,CAU1B"}
|