@snoai/memory 1.7.1 → 2.0.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.
- package/README.md +1 -1
- package/dist/agent-state-BB-voJsF.js +928 -0
- package/dist/agent-state-view.d.ts +1 -0
- package/dist/agent-state-view.js +185 -0
- package/dist/{atomic-memory-extraction-CWMMWF2C.js → atomic-memory-extraction-OusdO3Ig.js} +4 -4
- package/dist/client.js +1 -1
- package/dist/coding-skin-lewnGLT-.js +342 -0
- package/dist/coding-skin.d.ts +90 -4
- package/dist/coding-skin.js +3 -2
- package/dist/{config-D6GJyMKz.js → config-0oK6k24i.js} +2 -2
- package/dist/diagnostic-encoder.js +1 -1
- package/dist/index.js +5 -5
- package/dist/internal/config/settings.js +2 -2
- package/dist/internal/engine/bindings/memory-forget-tool.js +1 -1
- package/dist/internal/engine/bindings/memory-list-tool.js +1 -1
- package/dist/internal/engine/bindings/memory-recall-tool.js +1 -1
- package/dist/internal/engine/bindings/memory-reflection-resolve-tool.js +1 -1
- package/dist/internal/engine/bindings/memory-stats-tool.js +1 -1
- package/dist/internal/engine/bindings/memory-store-tool.js +3 -3
- package/dist/internal/engine/bindings/memory-tool-access.js +1 -1
- package/dist/internal/engine/bindings/memory-tool-results.js +1 -1
- package/dist/internal/engine/bindings/memory-tool-schemas.js +1 -1
- package/dist/internal/engine/bindings/memory-update-tool.js +2 -2
- package/dist/internal/engine/bindings/sno-station-mem-ambient-learning-hook.js +2 -2
- package/dist/internal/engine/bindings/sno-station-mem-auto-recall-hook.js +1 -1
- package/dist/internal/engine/bindings/sno-station-mem-insight-distill-factory.js +2 -2
- package/dist/internal/engine/bindings/sno-station-mem-message-transcript.js +2 -2
- package/dist/internal/engine/extraction/atomic-memory-extraction.js +1 -1
- package/dist/internal/engine/extraction/memory-temporality-classifier.js +1 -1
- package/dist/internal/engine/observability/observable-memory-store.js +1 -1
- package/dist/internal/engine/observability/runtime-diagnostics.js +1 -1
- package/dist/internal/engine/reflection/reflection-command-hooks.js +1 -1
- package/dist/internal/engine/reflection/reflection-embedded-generator.js +1 -1
- package/dist/internal/engine/reflection/reflection-injection-hooks.js +1 -1
- package/dist/internal/engine/reflection/reflection-lifecycle-hooks.js +1 -1
- package/dist/internal/engine/reflection/strategy-hook-runner.js +2 -2
- package/dist/internal/model/llm-client.js +1 -1
- package/dist/internal/store/maintenance.js +3 -3
- package/dist/internal/store/store.js +1 -1
- package/dist/{llm-client-BtbX4af4.js → llm-client-DhBvu-U_.js} +1 -1
- package/dist/{log-site-catalog.generated-Dtiih6m2.js → log-site-catalog.generated-ClhbzmWf.js} +2 -2
- package/dist/log-site-catalog.json +1 -1
- package/dist/memdump.js +1 -1
- package/dist/{memory-runtime-B9fG-sNo.js → memory-runtime-DPrBxPA5.js} +4 -4
- package/dist/{memory-temporality-classifier-Bm0EiCN8.js → memory-temporality-classifier-DTRhzXmG.js} +1 -1
- package/dist/{memory-tool-dependencies-BxVJ-bHs.js → memory-tool-dependencies-BQHE4kMD.js} +1 -1
- package/dist/{profile-section-writer-DSX6R3GF.js → profile-section-writer-QiB0MMtt.js} +2 -2
- package/dist/{reflection-command-hooks-Dyckr_Ma.js → reflection-command-hooks-DBpqpF7A.js} +3 -3
- package/dist/{reflection-embedded-generator-i8KHqMpr.js → reflection-embedded-generator-DNFMQNsv.js} +1 -1
- package/dist/{rem-batch-executor-BSWXFOLH.js → rem-batch-executor-CUD8VH49.js} +1 -1
- package/dist/{rem-trigger-BaJa5FS0.js → rem-trigger-BwdTBHtj.js} +29 -7
- package/dist/{server-DPd3Xef2.js → server-D7tIZPWW.js} +4 -4
- package/dist/sidecar/main.js +2 -2
- package/dist/{sno-station-mem-runtime-dependencies-DITNvfkR.js → sno-station-mem-runtime-dependencies-Fq6yRstU.js} +5 -5
- package/dist/{store-YX1_c1NH.js → store-cu7Qq2OW.js} +1 -1
- package/dist/{strategy-hook-runner-CwqZ1orf.js → strategy-hook-runner-CvdsVcwR.js} +1 -1
- package/package.json +1 -1
- package/settings.default.json +2 -2
- package/dist/coding-skin-BnFryVac.js +0 -265
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as readAgentView, c as screenAgents, n as compactAgentView, o as recordAgentHandoff, r as markAgentResultSeen, s as renderAgentScreen } from "./agent-state-BB-voJsF.js";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { emitKeypressEvents } from "node:readline";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
// config/agent-state-cli.ts
|
|
7
|
+
const execute = promisify(execFile);
|
|
8
|
+
function option(args, name) {
|
|
9
|
+
const index = args.indexOf(name);
|
|
10
|
+
return index < 0 ? void 0 : args[index + 1];
|
|
11
|
+
}
|
|
12
|
+
function renderAgentSnapshot(view) {
|
|
13
|
+
const lines = [`Sno Station · squad · ${view.observedAt}`, ""];
|
|
14
|
+
for (const group of [
|
|
15
|
+
"Needs you",
|
|
16
|
+
"Working",
|
|
17
|
+
"Finished",
|
|
18
|
+
"unknown"
|
|
19
|
+
]) {
|
|
20
|
+
lines.push(group === "unknown" ? "Unknown observations" : group);
|
|
21
|
+
for (const row of view.agents.filter((row) => row.group === group)) {
|
|
22
|
+
lines.push(` ${row.harness} ${row.sessionId} · ${row.subgroup} · ${row.ageSeconds}s ago`, ` Doing: ${row.doing} · Process: ${row.process} · Resumable: ${row.resumable} · Waiting for a human: ${row.waitingForHuman}`, ` Reason: ${row.waitingReason} · Reach: ${row.reachAddress} · Project: ${row.cwd} · Task: ${row.description}`, ` Work: ${JSON.stringify(row.work)} · Delivery: ${row.delivery} · Resume: ${row.resumeEvidence}`, ` Result: ${row.resultId} · Seen: ${row.humanSeen} · Last event: ${JSON.stringify(row.lastEvent)}`);
|
|
23
|
+
if (row.missingReasons.length) lines.push(` Missing: ${row.missingReasons.join("; ")}`);
|
|
24
|
+
}
|
|
25
|
+
lines.push("");
|
|
26
|
+
}
|
|
27
|
+
if (!view.agents.length) lines.push("No observed agents.");
|
|
28
|
+
lines.push("Quota", ...view.quota.length ? view.quota.map((item) => JSON.stringify(item)) : ["unknown · no account observation"]);
|
|
29
|
+
if (view.errors.length) lines.push("Operation errors", ...view.errors.map((error) => `${error.operation}: ${error.target}: ${error.error} (${error.impact})`));
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
/** Explicit account read only; local snapshots and status redraw never invoke this. */
|
|
33
|
+
async function refreshAgentQuota(view) {
|
|
34
|
+
let output;
|
|
35
|
+
let code = 0;
|
|
36
|
+
try {
|
|
37
|
+
output = (await execute("sno", [
|
|
38
|
+
"subscription-quota-check",
|
|
39
|
+
"--vendor",
|
|
40
|
+
"both",
|
|
41
|
+
"--json",
|
|
42
|
+
"--quiet"
|
|
43
|
+
])).stdout;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const failed = error instanceof Error ? error : new Error(String(error));
|
|
46
|
+
output = "stdout" in failed && typeof failed.stdout === "string" ? failed.stdout : "";
|
|
47
|
+
code = "code" in failed && typeof failed.code === "number" ? failed.code : -1;
|
|
48
|
+
view.errors.push({
|
|
49
|
+
operation: "read account quota",
|
|
50
|
+
target: "subscription-quota-check",
|
|
51
|
+
error: failed.message,
|
|
52
|
+
impact: "unavailable account quota remains unknown"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(output);
|
|
57
|
+
if (!parsed || typeof parsed !== "object" || !("vendors" in parsed) || !Array.isArray(parsed.vendors)) throw new Error("quota reader returned no vendor observations");
|
|
58
|
+
const observedAt = "captured_at" in parsed ? parsed.captured_at : "unknown";
|
|
59
|
+
view.quota = parsed.vendors.map((vendor) => ({
|
|
60
|
+
observation: vendor,
|
|
61
|
+
source: "subscription-quota-check",
|
|
62
|
+
observedAt,
|
|
63
|
+
exitCode: code
|
|
64
|
+
}));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
view.errors.push({
|
|
67
|
+
operation: "parse account quota",
|
|
68
|
+
target: "subscription-quota-check",
|
|
69
|
+
error: String(error),
|
|
70
|
+
impact: "quota unknown"
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function screenView() {
|
|
75
|
+
return screenAgents(await readAgentView());
|
|
76
|
+
}
|
|
77
|
+
async function screen() {
|
|
78
|
+
let { view, hidden } = await screenView();
|
|
79
|
+
let selected = 0;
|
|
80
|
+
let result = "";
|
|
81
|
+
const wasRaw = process.stdin.isRaw;
|
|
82
|
+
const reread = async () => {
|
|
83
|
+
const current = view.agents[selected];
|
|
84
|
+
({view, hidden} = await screenView());
|
|
85
|
+
const index = view.agents.findIndex((row) => row.harness === current?.harness && row.sessionId === current?.sessionId);
|
|
86
|
+
selected = index >= 0 ? index : Math.max(0, Math.min(selected, view.agents.length - 1));
|
|
87
|
+
};
|
|
88
|
+
const draw = () => {
|
|
89
|
+
process.stdout.write(`\x1b[H\x1b[2J${renderAgentScreen(view, selected, {
|
|
90
|
+
columns: process.stdout.columns ?? 80,
|
|
91
|
+
rows: process.stdout.rows ?? 24
|
|
92
|
+
}, result, hidden)}`);
|
|
93
|
+
};
|
|
94
|
+
process.stdout.on("resize", draw);
|
|
95
|
+
process.stdout.write("\x1B[?1049h\x1B[?25l");
|
|
96
|
+
emitKeypressEvents(process.stdin);
|
|
97
|
+
process.stdin.setRawMode(true);
|
|
98
|
+
process.stdin.resume();
|
|
99
|
+
draw();
|
|
100
|
+
try {
|
|
101
|
+
await new Promise((resolve) => {
|
|
102
|
+
const close = () => {
|
|
103
|
+
process.stdin.off("keypress", keypress);
|
|
104
|
+
process.off("SIGTERM", close);
|
|
105
|
+
process.off("SIGINT", close);
|
|
106
|
+
resolve();
|
|
107
|
+
};
|
|
108
|
+
const keypress = async (value, key) => {
|
|
109
|
+
if (value === "q" || key.name === "escape" || key.ctrl && key.name === "c") {
|
|
110
|
+
close();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
if (key.name === "down" || value === "j") {
|
|
115
|
+
selected = Math.min(view.agents.length - 1, selected + 1);
|
|
116
|
+
result = "";
|
|
117
|
+
} else if (key.name === "up" || value === "k") {
|
|
118
|
+
selected = Math.max(0, selected - 1);
|
|
119
|
+
result = "";
|
|
120
|
+
} else if (key.name === "return") {
|
|
121
|
+
const row = view.agents[selected];
|
|
122
|
+
if (row) {
|
|
123
|
+
result = `Result: ${row.finalReply}`;
|
|
124
|
+
await markAgentResultSeen(row);
|
|
125
|
+
await reread();
|
|
126
|
+
}
|
|
127
|
+
} else if (value === "r") {
|
|
128
|
+
await refreshAgentQuota(view);
|
|
129
|
+
result = `Quota: ${view.quota.map((item) => JSON.stringify(item.observation ?? item)).join(" · ")}`;
|
|
130
|
+
} else if (value === " ") {
|
|
131
|
+
const quota = view.quota;
|
|
132
|
+
await reread();
|
|
133
|
+
view.quota = quota;
|
|
134
|
+
}
|
|
135
|
+
draw();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
result = `refresh failed: ${String(error)}`;
|
|
138
|
+
draw();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
process.stdin.on("keypress", keypress);
|
|
142
|
+
process.on("SIGTERM", close);
|
|
143
|
+
process.on("SIGINT", close);
|
|
144
|
+
});
|
|
145
|
+
} finally {
|
|
146
|
+
process.stdout.off("resize", draw);
|
|
147
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
148
|
+
process.stdin.pause();
|
|
149
|
+
process.stdout.write("\x1B[?25h\x1B[?1049l");
|
|
150
|
+
}
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
async function agentStateMain(args) {
|
|
154
|
+
if (args.includes("--record-release")) {
|
|
155
|
+
let text = "";
|
|
156
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
157
|
+
await recordAgentHandoff(option(args, "--source") ?? "", option(args, "--target") ?? "", text);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
if (args.includes("--help")) {
|
|
161
|
+
process.stdout.write("sno squad [global --json] · local squad observations\n");
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
if (!args.includes("--json") && !args.includes("--compact") && process.stdout.isTTY && process.stdin.isTTY) return screen();
|
|
165
|
+
const view = await readAgentView({ recent: args.includes("--compact") && !args.includes("--json") });
|
|
166
|
+
if (args.includes("--json")) process.stdout.write(`${JSON.stringify(view)}\n`);
|
|
167
|
+
else if (args.includes("--compact")) process.stdout.write(`${compactAgentView(view, {
|
|
168
|
+
harness: option(args, "--harness"),
|
|
169
|
+
sessionId: option(args, "--session-id"),
|
|
170
|
+
reachAddress: option(args, "--reach-address")
|
|
171
|
+
})}\n`);
|
|
172
|
+
else process.stdout.write(`${renderAgentSnapshot(view)}\n`);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
agentStateMain(process.argv.slice(2)).then((code) => {
|
|
176
|
+
process.exitCode = code;
|
|
177
|
+
}, (error) => {
|
|
178
|
+
process.stderr.write(`${JSON.stringify({
|
|
179
|
+
operation: "read agent inventory",
|
|
180
|
+
error: String(error),
|
|
181
|
+
impact: "squad unavailable"
|
|
182
|
+
})}\n`);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
});
|
|
185
|
+
export {};
|
|
@@ -3,21 +3,21 @@ import "./internal/engine/i18n/locales.js";
|
|
|
3
3
|
import { $ as FIXED_MEMORY_SNO_EXTRACT_PROFILE, Q as FIXED_MEMORY_SNO_EXTRACT_CHAT } from "./signed-registry-constants-CO2vv_QJ.js";
|
|
4
4
|
import { redactSecrets } from "./internal/engine/security/redact.js";
|
|
5
5
|
import { t as appendAuditEntry } from "./runtime-audit-log-4NJsojEj.js";
|
|
6
|
-
import { a as isTerminalLlmFailure, i as createLlmClient, n as LlmClientTerminalError } from "./llm-client-
|
|
6
|
+
import { a as isTerminalLlmFailure, i as createLlmClient, n as LlmClientTerminalError } from "./llm-client-DhBvu-U_.js";
|
|
7
7
|
import { n as readModelReplyJson } from "./model-reply-text-C7jEWDXG.js";
|
|
8
8
|
import { resolveLlmRoute } from "./internal/model/llm-mode-routing.js";
|
|
9
9
|
import { n as ATOMIC_EXTRACTION_SKILL_HASH, r as atomicExtractionSkillReference, t as ATOMIC_EXTRACTION_SKILL } from "./atomic-extraction-skill-5lAqr8BM.js";
|
|
10
10
|
import { stripEnvelopeMetadata } from "./internal/engine/extraction/extraction-text-sanitizer.js";
|
|
11
11
|
import { a as sessionZoneCarriedBy, t as calculateCalendarTime } from "./calendar-instruction-uK6xsrCt.js";
|
|
12
|
-
import "./memory-temporality-classifier-
|
|
12
|
+
import "./memory-temporality-classifier-DTRhzXmG.js";
|
|
13
13
|
import { a as parseAtomicCaptureReply, c as atomic_extraction_response_schema_default, i as fallbackAtomicCapturedFact, l as excludeProgressRecords, n as ATOMIC_ENRICHMENT_RESPONSE_JSON_SCHEMA, o as parseAtomicEnrichmentReply, r as ATOMIC_EXTRACTION_RESPONSE_JSON_SCHEMA, s as parseAtomicExtractionReply, t as ATOMIC_CAPTURE_RESPONSE_JSON_SCHEMA } from "./atomic-extraction-reply-CoxIGLmU.js";
|
|
14
14
|
import { adapterAViewFromRecord, isAdapterAVerdictReply, parseAdapterAChatVerdict, renderAdapterAChatPrompt, renderAdapterAPrompt } from "./internal/engine/rem/index.js";
|
|
15
15
|
import { n as state_vocabulary_default, r as attribute_dictionary_default, t as relations } from "./relation-dictionary-QVBewRQ1.js";
|
|
16
16
|
import { g as randomUUID$1 } from "./memory-store-shared-B-zOxk_Q.js";
|
|
17
|
-
import { b as normalizeMemoryRelationPredicate, d as normalizeEntityName, f as recordAtomicEntityIdentityJournal, g as readAtomicArrivalRetirementCandidateSet, h as journalAtomicArrivalRetirementRefusal, m as closeAtomicArrivalRetirementTargets, u as listAtomicMemoryEntityCandidates, v as AtomicExtractionCompletedElsewhere } from "./store-
|
|
17
|
+
import { b as normalizeMemoryRelationPredicate, d as normalizeEntityName, f as recordAtomicEntityIdentityJournal, g as readAtomicArrivalRetirementCandidateSet, h as journalAtomicArrivalRetirementRefusal, m as closeAtomicArrivalRetirementTargets, u as listAtomicMemoryEntityCandidates, v as AtomicExtractionCompletedElsewhere } from "./store-cu7Qq2OW.js";
|
|
18
18
|
import { o as normalizeTopicToSectionName } from "./b-profile-section-dictionary-provider-DMGsJlYh.js";
|
|
19
19
|
import { r as recordTokenCounter } from "./memory-store-write-validation-BbvoN3Li.js";
|
|
20
|
-
import { i as prepareDeterministicTaskLifecycleWrite, t as PROFILE_IDENTITY_SECTION } from "./profile-section-writer-
|
|
20
|
+
import { i as prepareDeterministicTaskLifecycleWrite, t as PROFILE_IDENTITY_SECTION } from "./profile-section-writer-QiB0MMtt.js";
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { countTokens } from "@snoai/chunking";
|
|
23
23
|
import { createLogger, currentLogContext, privateLogReference, withLogContext } from "@snoai/utils/logger";
|
package/dist/client.js
CHANGED
|
@@ -5,9 +5,9 @@ import { getPrincipal, getSettingsPath, getSnoStationMemStateDir, getStartupLogP
|
|
|
5
5
|
import { checkDiscovery, processAlive, readDiscovery } from "./internal/contract/discovery.js";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { mkdir, open } from "node:fs/promises";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
8
9
|
import { readFileSync } from "node:fs";
|
|
9
10
|
import { request } from "node:http";
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
11
|
import { connect as connect$1 } from "node:net";
|
|
12
12
|
import { setTimeout } from "node:timers/promises";
|
|
13
13
|
// src/contract/start.ts
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { a as readAgentView, n as compactAgentView } from "./agent-state-BB-voJsF.js";
|
|
2
|
+
import "./internal/engine/telemetry/observe-ledger.js";
|
|
3
|
+
import "./internal/config/skill-categories.js";
|
|
4
|
+
import "./internal/config/skin-defaults.js";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { getSnoProfileDir } from "@snoai/observability";
|
|
7
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
8
|
+
import { execFile, spawn } from "node:child_process";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
// config/agent-statusline.ts
|
|
11
|
+
function record(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
13
|
+
}
|
|
14
|
+
function originalStatusLine(command, harness) {
|
|
15
|
+
if (typeof command !== "string" || !command.includes(`/mem-${harness}/dist/cli.js' status-line `)) return void 0;
|
|
16
|
+
const encoded = / --sno-original '([A-Za-z0-9+/=]+)'$/.exec(command)?.[1];
|
|
17
|
+
if (!encoded) return void 0;
|
|
18
|
+
const values = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
19
|
+
if (!Array.isArray(values)) throw new Error("invalid original status-line configuration");
|
|
20
|
+
return { value: values[0] };
|
|
21
|
+
}
|
|
22
|
+
/** The owned command carries the original configuration; no separate settings backup is needed. */
|
|
23
|
+
function composeAgentStatusLine(config, entryPath, harness, installing) {
|
|
24
|
+
const previous = originalStatusLine(record(config.statusLine)?.command, harness);
|
|
25
|
+
if (!installing && !previous) return;
|
|
26
|
+
const original = previous ? previous.value : config.statusLine;
|
|
27
|
+
if (!installing) {
|
|
28
|
+
if (original === void 0) delete config.statusLine;
|
|
29
|
+
else config.statusLine = original;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const encoded = Buffer.from(JSON.stringify(original === void 0 ? [] : [original])).toString("base64");
|
|
33
|
+
config.statusLine = {
|
|
34
|
+
...record(original),
|
|
35
|
+
type: "command",
|
|
36
|
+
command: `${shellQuote(process.execPath)} ${shellQuote(entryPath)} status-line --sno-original '${encoded}'`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function userOutput(command, input) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const child = spawn("sh", ["-c", command], { stdio: [
|
|
42
|
+
"pipe",
|
|
43
|
+
"pipe",
|
|
44
|
+
"inherit"
|
|
45
|
+
] });
|
|
46
|
+
const chunks = [];
|
|
47
|
+
child.stdout.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
48
|
+
child.on("error", reject);
|
|
49
|
+
child.on("close", (code) => resolve({
|
|
50
|
+
output: Buffer.concat(chunks),
|
|
51
|
+
code: code ?? 1
|
|
52
|
+
}));
|
|
53
|
+
child.stdin.on("error", (error) => {
|
|
54
|
+
if (!("code" in error) || error.code !== "EPIPE") reject(error);
|
|
55
|
+
});
|
|
56
|
+
child.stdin.end(input);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function renderAgentStatusLine(harness, originalEncoded, input) {
|
|
60
|
+
const originals = JSON.parse(Buffer.from(originalEncoded, "base64").toString("utf8"));
|
|
61
|
+
const command = Array.isArray(originals) ? record(originals[0])?.command : void 0;
|
|
62
|
+
const user = typeof command === "string" ? await userOutput(command, input) : {
|
|
63
|
+
output: Buffer.alloc(0),
|
|
64
|
+
code: 0
|
|
65
|
+
};
|
|
66
|
+
const newlineLength = user.output.at(-1) === 10 ? user.output.at(-2) === 13 ? 2 : 1 : 0;
|
|
67
|
+
process.stdout.write(user.output.subarray(0, user.output.length - newlineLength));
|
|
68
|
+
try {
|
|
69
|
+
const sessionId = record(JSON.parse(input))?.session_id;
|
|
70
|
+
if (typeof sessionId !== "string" || !sessionId) throw new Error("current client session identity unavailable");
|
|
71
|
+
const view = await readAgentView({ recent: true });
|
|
72
|
+
const segment = compactAgentView(view, {
|
|
73
|
+
harness,
|
|
74
|
+
sessionId
|
|
75
|
+
});
|
|
76
|
+
const color = /\bneed you\b/.test(segment) ? "\x1B[1;33m" : "\x1B[2m";
|
|
77
|
+
const displayed = process.env.NO_COLOR === void 0 ? `${color}${segment}\x1b[0m` : segment;
|
|
78
|
+
process.stdout.write(`${user.output.length > newlineLength ? " | " : ""}${displayed}\n`);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
process.stdout.write(user.output.subarray(user.output.length - newlineLength));
|
|
81
|
+
process.stderr.write(`${JSON.stringify({
|
|
82
|
+
operation: "agent-state-status-line",
|
|
83
|
+
target: harness,
|
|
84
|
+
error: String(error),
|
|
85
|
+
impact: "Sno segment unavailable; original output retained"
|
|
86
|
+
})}\n`);
|
|
87
|
+
}
|
|
88
|
+
return user.code;
|
|
89
|
+
}
|
|
90
|
+
// config/work-continuity.ts
|
|
91
|
+
function object(value) {
|
|
92
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
function failure(operation, target, error, impact) {
|
|
95
|
+
console.error(JSON.stringify({
|
|
96
|
+
operation,
|
|
97
|
+
target,
|
|
98
|
+
error: String(error),
|
|
99
|
+
impact
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
function continuityBriefPath(sessionId) {
|
|
103
|
+
return join(getSnoProfileDir(), "station", "handoff", `${sessionId}.md`);
|
|
104
|
+
}
|
|
105
|
+
function context(output, target) {
|
|
106
|
+
if (!output.trim()) return "";
|
|
107
|
+
try {
|
|
108
|
+
const envelope = JSON.parse(output);
|
|
109
|
+
if (object(envelope) && object(envelope["hookSpecificOutput"])) {
|
|
110
|
+
const text = envelope["hookSpecificOutput"]["additionalContext"];
|
|
111
|
+
if (typeof text === "string") return text;
|
|
112
|
+
}
|
|
113
|
+
throw new Error("no hookSpecificOutput.additionalContext");
|
|
114
|
+
} catch (error) {
|
|
115
|
+
failure("read hook context", target, error, "this context was not delivered; other task context remains available");
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function sno(args, input, timeoutMs, env = process.env) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const child = execFile(env["SNO_EXECUTABLE"] ?? "sno", args, {
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
timeout: timeoutMs,
|
|
124
|
+
maxBuffer: Number.POSITIVE_INFINITY,
|
|
125
|
+
env
|
|
126
|
+
}, (error, stdout, stderr) => {
|
|
127
|
+
if (stderr) process.stderr.write(stderr);
|
|
128
|
+
if (error) failure(args.slice(0, 2).join(" "), String(input["session_id"]), error, "this operation failed; the saved brief and other available context remain usable");
|
|
129
|
+
resolve(error ? "" : stdout);
|
|
130
|
+
});
|
|
131
|
+
child.stdin?.on("error", (error) => failure("write hook input", args.join(" "), error, "this command did not receive its hook input"));
|
|
132
|
+
child.stdin?.end(JSON.stringify(input));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Reuse the memory hook's own deadline for the parallel lesson lookup. */
|
|
136
|
+
async function recallDeadline(event) {
|
|
137
|
+
const hook = event === "UserPromptSubmit" ? CODING_SKIN_HOOKS.UserPromptSubmit : CODING_SKIN_HOOKS.SessionStart;
|
|
138
|
+
try {
|
|
139
|
+
const settings = JSON.parse(await readFile(join(getSnoProfileDir(), "settings.json"), "utf8"));
|
|
140
|
+
const key = event === "UserPromptSubmit" ? "prompt" : "sessionStart";
|
|
141
|
+
if (object(settings) && object(settings["recall"]) && object(settings["recall"][key])) {
|
|
142
|
+
const timeoutMs = settings["recall"][key]["timeoutMs"];
|
|
143
|
+
if (typeof timeoutMs === "number" && timeoutMs > 0) return timeoutMs;
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
failure("read recall deadline", join(getSnoProfileDir(), "settings.json"), error, "using the installed hook deadline; local continuity remains enabled");
|
|
147
|
+
}
|
|
148
|
+
return hook.timeout * 1e3;
|
|
149
|
+
}
|
|
150
|
+
/** The same envelope carries durable task state, memory recall and locally approved lessons. */
|
|
151
|
+
async function composeContinuityContext(harness, event, input, memory) {
|
|
152
|
+
if (!object(input) || typeof input["session_id"] !== "string" || typeof input["cwd"] !== "string" || input["agent_id"] !== void 0) return JSON.stringify({ hookSpecificOutput: {
|
|
153
|
+
hookEventName: event,
|
|
154
|
+
additionalContext: ""
|
|
155
|
+
} });
|
|
156
|
+
const path = continuityBriefPath(input["session_id"]);
|
|
157
|
+
const lessons = recallDeadline(event).then((timeoutMs) => sno([
|
|
158
|
+
"rem-reflect",
|
|
159
|
+
"recall",
|
|
160
|
+
"--agent",
|
|
161
|
+
harness === "claude" ? "claude-code" : "codex",
|
|
162
|
+
"--first-message"
|
|
163
|
+
], {
|
|
164
|
+
...input,
|
|
165
|
+
hook_event_name: event
|
|
166
|
+
}, timeoutMs));
|
|
167
|
+
const recalled = memory();
|
|
168
|
+
const readBrief = () => readFile(path, "utf8").catch((error) => {
|
|
169
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) failure("read handoff brief", path, error, "task restoration from this brief failed; other context remains available");
|
|
170
|
+
return "";
|
|
171
|
+
});
|
|
172
|
+
const brief = event !== "SessionStart" ? readBrief() : (async () => {
|
|
173
|
+
const sessionId = String(input["session_id"]);
|
|
174
|
+
try {
|
|
175
|
+
await mkdir(dirname(path), {
|
|
176
|
+
recursive: true,
|
|
177
|
+
mode: 448
|
|
178
|
+
});
|
|
179
|
+
if (await readFile(path, "utf8").then(() => true, () => false)) return readBrief();
|
|
180
|
+
await sno([
|
|
181
|
+
"handoff-checkpoint",
|
|
182
|
+
"--repo",
|
|
183
|
+
String(input["cwd"]),
|
|
184
|
+
path
|
|
185
|
+
], input, await recallDeadline(event), checkpointEnvironment(harness, sessionId));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
failure("prepare handoff brief", path, error, "the agent has no brief template; available context is still delivered");
|
|
188
|
+
}
|
|
189
|
+
return "";
|
|
190
|
+
})();
|
|
191
|
+
const [saved, memoryRecall, approved] = await Promise.allSettled([
|
|
192
|
+
brief,
|
|
193
|
+
recalled,
|
|
194
|
+
lessons
|
|
195
|
+
]);
|
|
196
|
+
const parts = [];
|
|
197
|
+
if (event === "SessionStart") parts.push(`Keep the current task in the readable handoff brief at ${path}; its template already exists. Start the user's task first: do not read or edit the brief before your first action on the task. Update the brief only when there is real progress to record (a finished step, a decision, a failure, or before an orderly interruption), and only with your ${harness === "claude" ? "Edit or Write tool" : "apply_patch tool"}, never through a shell command. Keep its readable sections accurate: Objective and authorization, decisions and exclusions, Done/current files, Next with the exact next action and its expected observable result, Known failure with operation/target/error, Reach card/recipient when present, and Report destination. Do not ask the owner to reconstruct lost work.`);
|
|
198
|
+
if (saved.status === "fulfilled" && saved.value) parts.push(`Current task handoff brief (complete file: ${path}). If the host reports context overflow, read that complete file before task action; do not infer the missing text. Reconcile actual file/tool results before repeating an unfinished action.\n\n${saved.value}`);
|
|
199
|
+
if (memoryRecall.status === "fulfilled") parts.push(context(memoryRecall.value, "memory recall"));
|
|
200
|
+
else failure("memory recall", input["session_id"], memoryRecall.reason, "memory recall failed; the saved brief and approved lessons remain available");
|
|
201
|
+
if (approved.status === "fulfilled") parts.push(context(approved.value, join(process.env["REM_REFLECT_STORE"] ?? join(homedir(), ".sno", "experience"), "wiki", "lessons.jsonl")));
|
|
202
|
+
else failure("read approved lessons", input["session_id"], approved.reason, "lesson delivery failed; task context remains available");
|
|
203
|
+
return JSON.stringify({ hookSpecificOutput: {
|
|
204
|
+
hookEventName: event,
|
|
205
|
+
additionalContext: parts.filter(Boolean).join("\n\n")
|
|
206
|
+
} });
|
|
207
|
+
}
|
|
208
|
+
/** Refresh the existing readable brief before compaction; never manufacture task facts. */
|
|
209
|
+
async function refreshContinuityBrief(harness, input) {
|
|
210
|
+
if (!object(input) || typeof input["session_id"] !== "string" || typeof input["cwd"] !== "string" || input["agent_id"] !== void 0) return;
|
|
211
|
+
const path = continuityBriefPath(input["session_id"]);
|
|
212
|
+
try {
|
|
213
|
+
await readFile(path, "utf8");
|
|
214
|
+
} catch (error) {
|
|
215
|
+
failure("save before compaction", path, error, "no readable current brief was saved; recovery can use only previously durable records");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
await sno([
|
|
219
|
+
"handoff-checkpoint",
|
|
220
|
+
"--repo",
|
|
221
|
+
input["cwd"],
|
|
222
|
+
path
|
|
223
|
+
], input, CODING_SKIN_HOOKS.Stop.timeout * 1e3, checkpointEnvironment(harness, input["session_id"]));
|
|
224
|
+
}
|
|
225
|
+
function checkpointEnvironment(harness, sessionId) {
|
|
226
|
+
return {
|
|
227
|
+
...process.env,
|
|
228
|
+
CLAUDE_SESSION_ID: harness === "claude" ? sessionId : void 0,
|
|
229
|
+
CODEX_THREAD_ID: harness === "codex" ? sessionId : void 0
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// config/coding-skin.ts
|
|
233
|
+
const CODING_SKIN_CHILD_DEADLINE_MS = 11e4;
|
|
234
|
+
const CODING_SKIN_WORKER_LIFETIME_MS = 54e4;
|
|
235
|
+
const CODING_SKIN_RETRY_DELAYS_MS = [2e3, 1e4];
|
|
236
|
+
const CODING_SKIN_MAX_ATTEMPTS = 3;
|
|
237
|
+
const CODING_SKIN_MAX_PENDING_CAPTURES = 1e3;
|
|
238
|
+
/** A session's turns wait in the spool until the session ends or has had no new turn for this long, then go together. */
|
|
239
|
+
const CODING_SKIN_CAPTURE_QUIET_MS = 18e5;
|
|
240
|
+
/**
|
|
241
|
+
* Input per capture, estimated as UTF-8 bytes / 3 (at or above the sidecar's CJK-aware count). It keeps one capture to
|
|
242
|
+
* about one 4,096-token extraction window, so a capture ends well inside its route deadline and a failed one retries
|
|
243
|
+
* a small part; the rest is room for the window's JSON framing.
|
|
244
|
+
*/
|
|
245
|
+
const CODING_SKIN_CAPTURE_MAX_INPUT_TOKENS = 3500;
|
|
246
|
+
const CODING_SKIN_IMPORT_INTERVAL_MS = 2010;
|
|
247
|
+
const CODING_SKIN_CHILD_MAX_BUFFER_BYTES = 8388608;
|
|
248
|
+
const CODING_SKIN_MANUAL_SESSION_ID = "manual";
|
|
249
|
+
const CODING_SKIN_SESSION_QUERY = "standing decisions, open tasks, conventions and known pitfalls for this repository";
|
|
250
|
+
const CODING_SKIN_LEDGER_MAX_CHARS = 12e3;
|
|
251
|
+
const CODING_SKIN_RECALL_SENTENCE_MAX_CHARS = 200;
|
|
252
|
+
const CODING_SKIN_RECALL_ITEM_MAX_CHARS = 240;
|
|
253
|
+
const CODING_SKIN_HOOKS = {
|
|
254
|
+
SessionStart: {
|
|
255
|
+
eventName: "session_start",
|
|
256
|
+
subcommand: "session-start",
|
|
257
|
+
timeout: 15
|
|
258
|
+
},
|
|
259
|
+
UserPromptSubmit: {
|
|
260
|
+
eventName: "user_prompt_submit",
|
|
261
|
+
subcommand: "user-prompt-submit",
|
|
262
|
+
timeout: 30
|
|
263
|
+
},
|
|
264
|
+
Stop: {
|
|
265
|
+
eventName: "stop",
|
|
266
|
+
subcommand: "stop",
|
|
267
|
+
timeout: 5
|
|
268
|
+
},
|
|
269
|
+
SessionEnd: {
|
|
270
|
+
eventName: "session_end",
|
|
271
|
+
subcommand: "session-end",
|
|
272
|
+
timeout: 8
|
|
273
|
+
},
|
|
274
|
+
PreToolUse: {
|
|
275
|
+
eventName: "pre_tool_use",
|
|
276
|
+
subcommand: "pre-tool-use",
|
|
277
|
+
timeout: 5
|
|
278
|
+
},
|
|
279
|
+
PostToolUse: {
|
|
280
|
+
eventName: "post_tool_use",
|
|
281
|
+
subcommand: "post-tool-use",
|
|
282
|
+
timeout: 30
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
/** Cursor's hook events (`hooks.json` names); timeouts match the Claude/Codex event each one stands in for. */
|
|
286
|
+
const CODING_SKIN_CURSOR_HOOKS = {
|
|
287
|
+
sessionStart: {
|
|
288
|
+
subcommand: "session-start",
|
|
289
|
+
timeout: 15
|
|
290
|
+
},
|
|
291
|
+
beforeSubmitPrompt: {
|
|
292
|
+
subcommand: "user-prompt-submit",
|
|
293
|
+
timeout: 30
|
|
294
|
+
},
|
|
295
|
+
afterAgentResponse: {
|
|
296
|
+
subcommand: "after-agent-response",
|
|
297
|
+
timeout: 5
|
|
298
|
+
},
|
|
299
|
+
afterAgentThought: {
|
|
300
|
+
subcommand: "after-agent-thought",
|
|
301
|
+
timeout: 5
|
|
302
|
+
},
|
|
303
|
+
stop: {
|
|
304
|
+
subcommand: "stop",
|
|
305
|
+
timeout: 5
|
|
306
|
+
},
|
|
307
|
+
preCompact: {
|
|
308
|
+
subcommand: "pre-compact",
|
|
309
|
+
timeout: 5
|
|
310
|
+
},
|
|
311
|
+
sessionEnd: {
|
|
312
|
+
subcommand: "session-end",
|
|
313
|
+
timeout: 8
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const CODING_SKIN_MODEL_COMMANDS = [
|
|
317
|
+
"recall",
|
|
318
|
+
"get",
|
|
319
|
+
"remember",
|
|
320
|
+
"correct"
|
|
321
|
+
];
|
|
322
|
+
function shellQuote(value) {
|
|
323
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
324
|
+
}
|
|
325
|
+
/** Linux reports a running binary that was replaced on disk as "<path> (deleted)"; the path itself is still the right one. */
|
|
326
|
+
function snoProgramPath(path) {
|
|
327
|
+
return path.endsWith(" (deleted)") ? path.slice(0, -10) : path;
|
|
328
|
+
}
|
|
329
|
+
/** The command an agent hook runs: the `sno` binary that ran setup, then `memory hook <event> --harness <name>`. */
|
|
330
|
+
function codingSkinHookCommand(snoPath, subcommand, harness) {
|
|
331
|
+
return `${shellQuote(snoPath)} memory hook ${subcommand} --harness ${harness}`;
|
|
332
|
+
}
|
|
333
|
+
/** True for a hook command of that shape, whatever the path to `sno` is. */
|
|
334
|
+
function isCodingSkinHookCommand(command, subcommand, harness) {
|
|
335
|
+
const suffix = ` memory hook ${subcommand} --harness ${harness}`;
|
|
336
|
+
const trimmed = command.trim();
|
|
337
|
+
if (!trimmed.endsWith(suffix)) return false;
|
|
338
|
+
const encoded = trimmed.slice(0, -suffix.length);
|
|
339
|
+
const program = encoded.startsWith("'") && encoded.endsWith("'") ? encoded.slice(1, -1).replaceAll("'\\''", "'") : encoded;
|
|
340
|
+
return basename(snoProgramPath(program)) === "sno";
|
|
341
|
+
}
|
|
342
|
+
export { continuityBriefPath as C, renderAgentStatusLine as E, composeContinuityContext as S, composeAgentStatusLine as T, CODING_SKIN_WORKER_LIFETIME_MS as _, CODING_SKIN_CURSOR_HOOKS as a, shellQuote as b, CODING_SKIN_LEDGER_MAX_CHARS as c, CODING_SKIN_MAX_PENDING_CAPTURES as d, CODING_SKIN_MODEL_COMMANDS as f, CODING_SKIN_SESSION_QUERY as g, CODING_SKIN_RETRY_DELAYS_MS as h, CODING_SKIN_CHILD_MAX_BUFFER_BYTES as i, CODING_SKIN_MANUAL_SESSION_ID as l, CODING_SKIN_RECALL_SENTENCE_MAX_CHARS as m, CODING_SKIN_CAPTURE_QUIET_MS as n, CODING_SKIN_HOOKS as o, CODING_SKIN_RECALL_ITEM_MAX_CHARS as p, CODING_SKIN_CHILD_DEADLINE_MS as r, CODING_SKIN_IMPORT_INTERVAL_MS as s, CODING_SKIN_CAPTURE_MAX_INPUT_TOKENS as t, CODING_SKIN_MAX_ATTEMPTS as u, codingSkinHookCommand as v, refreshContinuityBrief as w, snoProgramPath as x, isCodingSkinHookCommand as y };
|