@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.100
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/AdoptionSpecialist.ouro/agent.json +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +147 -205
- package/assets/ouroboros.png +0 -0
- package/changelog.json +596 -0
- package/dist/heart/active-work.js +251 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/commitments.js +109 -0
- package/dist/heart/config.js +102 -23
- package/dist/heart/core.js +512 -94
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/auth-flow.js +430 -0
- package/dist/heart/daemon/daemon-cli.js +1935 -185
- package/dist/heart/daemon/daemon-entry.js +55 -6
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon.js +218 -9
- package/dist/heart/daemon/hatch-animation.js +35 -0
- package/dist/heart/daemon/hatch-flow.js +10 -83
- package/dist/heart/daemon/hatch-specialist.js +6 -1
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +147 -0
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/os-cron.js +260 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
- package/dist/heart/daemon/ouro-path-installer.js +260 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/process-manager.js +32 -2
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +61 -14
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +307 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +129 -0
- package/dist/heart/daemon/specialist-prompt.js +99 -0
- package/dist/heart/daemon/specialist-tools.js +283 -0
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/task-scheduler.js +4 -1
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +153 -23
- package/dist/heart/kicks.js +1 -19
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +191 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/providers/anthropic.js +77 -9
- package/dist/heart/providers/azure.js +86 -7
- package/dist/heart/providers/github-copilot.js +149 -0
- package/dist/heart/providers/minimax.js +4 -0
- package/dist/heart/providers/openai-codex.js +12 -3
- package/dist/heart/safe-workspace.js +381 -0
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/session-activity.js +169 -0
- package/dist/heart/session-recall.js +116 -0
- package/dist/heart/streaming.js +103 -22
- package/dist/heart/target-resolution.js +123 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +37 -4
- package/dist/mind/bundle-manifest.js +70 -0
- package/dist/mind/context.js +141 -11
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +43 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +9 -1
- package/dist/mind/memory.js +89 -26
- package/dist/mind/obligation-steering.js +31 -0
- package/dist/mind/pending.js +160 -0
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt-refresh.js +20 -0
- package/dist/mind/prompt.js +499 -8
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/file-completeness.js +14 -4
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/index.js +12 -0
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +210 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +69 -4
- package/dist/repertoire/coding/spawner.js +21 -3
- package/dist/repertoire/coding/tools.js +105 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +290 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +195 -0
- package/dist/repertoire/skills.js +3 -26
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/transitions.js +1 -2
- package/dist/repertoire/tools-base.js +770 -213
- package/dist/repertoire/tools-bluebubbles.js +93 -0
- package/dist/repertoire/tools-teams.js +58 -25
- package/dist/repertoire/tools.js +106 -53
- package/dist/senses/bluebubbles-client.js +484 -0
- package/dist/senses/bluebubbles-entry.js +13 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +261 -0
- package/dist/senses/bluebubbles-mutation-log.js +116 -0
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +1181 -0
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +452 -99
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +154 -0
- package/dist/senses/inner-dialog-worker.js +47 -18
- package/dist/senses/inner-dialog.js +387 -70
- package/dist/senses/pipeline.js +307 -0
- package/dist/senses/session-lock.js +119 -0
- package/dist/senses/teams.js +574 -129
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +16 -4
- package/subagents/README.md +4 -68
- package/dist/heart/daemon/subagent-installer.js +0 -125
- package/dist/inner-worker-entry.js +0 -4
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -593
- package/subagents/work-planner.md +0 -373
|
@@ -87,7 +87,19 @@ function readFacts(memoryRoot) {
|
|
|
87
87
|
const raw = fs.readFileSync(factsPath, "utf8").trim();
|
|
88
88
|
if (!raw)
|
|
89
89
|
return [];
|
|
90
|
-
|
|
90
|
+
const facts = [];
|
|
91
|
+
for (const line of raw.split("\n")) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed)
|
|
94
|
+
continue;
|
|
95
|
+
try {
|
|
96
|
+
facts.push(JSON.parse(trimmed));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Skip corrupt lines (e.g. partial write from a crash).
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return facts;
|
|
91
103
|
}
|
|
92
104
|
function getLatestUserText(messages) {
|
|
93
105
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -144,8 +156,29 @@ async function injectAssociativeRecall(messages, options) {
|
|
|
144
156
|
const facts = readFacts(memoryRoot);
|
|
145
157
|
if (facts.length === 0)
|
|
146
158
|
return;
|
|
147
|
-
|
|
148
|
-
|
|
159
|
+
let recalled;
|
|
160
|
+
try {
|
|
161
|
+
const provider = options?.provider ?? createDefaultProvider();
|
|
162
|
+
recalled = await recallFactsForQuery(query, facts, provider, options);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Embeddings unavailable — fall back to substring matching
|
|
166
|
+
const lowerQuery = query.toLowerCase();
|
|
167
|
+
const topK = options?.topK ?? DEFAULT_TOP_K;
|
|
168
|
+
recalled = facts
|
|
169
|
+
.filter((fact) => fact.text.toLowerCase().includes(lowerQuery))
|
|
170
|
+
.slice(0, topK)
|
|
171
|
+
.map((fact) => ({ ...fact, score: 1 }));
|
|
172
|
+
if (recalled.length > 0) {
|
|
173
|
+
(0, runtime_1.emitNervesEvent)({
|
|
174
|
+
level: "warn",
|
|
175
|
+
component: "mind",
|
|
176
|
+
event: "mind.associative_recall_fallback",
|
|
177
|
+
message: "embeddings unavailable, used substring fallback",
|
|
178
|
+
meta: { matchCount: recalled.length },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
149
182
|
if (recalled.length === 0)
|
|
150
183
|
return;
|
|
151
184
|
const recallSection = recalled
|
|
@@ -169,7 +202,7 @@ async function injectAssociativeRecall(messages, options) {
|
|
|
169
202
|
event: "mind.associative_recall_error",
|
|
170
203
|
message: "associative recall failed",
|
|
171
204
|
meta: {
|
|
172
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
205
|
+
reason: error instanceof Error ? error.message : /* v8 ignore start -- defensive: non-Error catch branch @preserve */ String(error) /* v8 ignore stop */,
|
|
173
206
|
},
|
|
174
207
|
});
|
|
175
208
|
}
|
|
@@ -34,6 +34,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CANONICAL_BUNDLE_MANIFEST = void 0;
|
|
37
|
+
exports.getChangelogPath = getChangelogPath;
|
|
38
|
+
exports.getPackageVersion = getPackageVersion;
|
|
39
|
+
exports.createBundleMeta = createBundleMeta;
|
|
40
|
+
exports.backfillBundleMeta = backfillBundleMeta;
|
|
41
|
+
exports.resetBackfillTracking = resetBackfillTracking;
|
|
37
42
|
exports.isCanonicalBundlePath = isCanonicalBundlePath;
|
|
38
43
|
exports.findNonCanonicalBundlePaths = findNonCanonicalBundlePaths;
|
|
39
44
|
const fs = __importStar(require("fs"));
|
|
@@ -41,6 +46,7 @@ const path = __importStar(require("path"));
|
|
|
41
46
|
const runtime_1 = require("../nerves/runtime");
|
|
42
47
|
exports.CANONICAL_BUNDLE_MANIFEST = [
|
|
43
48
|
{ path: "agent.json", kind: "file" },
|
|
49
|
+
{ path: "bundle-meta.json", kind: "file" },
|
|
44
50
|
{ path: "psyche/SOUL.md", kind: "file" },
|
|
45
51
|
{ path: "psyche/IDENTITY.md", kind: "file" },
|
|
46
52
|
{ path: "psyche/LORE.md", kind: "file" },
|
|
@@ -48,11 +54,75 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
|
|
|
48
54
|
{ path: "psyche/ASPIRATIONS.md", kind: "file" },
|
|
49
55
|
{ path: "psyche/memory", kind: "dir" },
|
|
50
56
|
{ path: "friends", kind: "dir" },
|
|
57
|
+
{ path: "state", kind: "dir" },
|
|
51
58
|
{ path: "tasks", kind: "dir" },
|
|
52
59
|
{ path: "skills", kind: "dir" },
|
|
53
60
|
{ path: "senses", kind: "dir" },
|
|
54
61
|
{ path: "senses/teams", kind: "dir" },
|
|
55
62
|
];
|
|
63
|
+
function getChangelogPath() {
|
|
64
|
+
const changelogPath = path.resolve(__dirname, "../../changelog.json");
|
|
65
|
+
(0, runtime_1.emitNervesEvent)({
|
|
66
|
+
component: "mind",
|
|
67
|
+
event: "mind.changelog_path_resolved",
|
|
68
|
+
message: "resolved changelog path",
|
|
69
|
+
meta: { path: changelogPath },
|
|
70
|
+
});
|
|
71
|
+
return changelogPath;
|
|
72
|
+
}
|
|
73
|
+
function getPackageVersion() {
|
|
74
|
+
const packageJsonPath = path.resolve(__dirname, "../../package.json");
|
|
75
|
+
const raw = fs.readFileSync(packageJsonPath, "utf-8");
|
|
76
|
+
const parsed = JSON.parse(raw);
|
|
77
|
+
(0, runtime_1.emitNervesEvent)({
|
|
78
|
+
component: "mind",
|
|
79
|
+
event: "mind.package_version_read",
|
|
80
|
+
message: "read package version",
|
|
81
|
+
meta: { version: parsed.version },
|
|
82
|
+
});
|
|
83
|
+
return parsed.version;
|
|
84
|
+
}
|
|
85
|
+
function createBundleMeta() {
|
|
86
|
+
return {
|
|
87
|
+
runtimeVersion: getPackageVersion(),
|
|
88
|
+
bundleSchemaVersion: 1,
|
|
89
|
+
lastUpdated: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const _backfilledRoots = new Set();
|
|
93
|
+
/**
|
|
94
|
+
* If bundle-meta.json is missing from the agent root, create it with current runtime version.
|
|
95
|
+
* This backfills existing agent bundles that were created before bundle-meta.json was introduced.
|
|
96
|
+
* Only attempts once per bundleRoot per process.
|
|
97
|
+
*/
|
|
98
|
+
function backfillBundleMeta(bundleRoot) {
|
|
99
|
+
if (_backfilledRoots.has(bundleRoot))
|
|
100
|
+
return;
|
|
101
|
+
_backfilledRoots.add(bundleRoot);
|
|
102
|
+
const metaPath = path.join(bundleRoot, "bundle-meta.json");
|
|
103
|
+
try {
|
|
104
|
+
if (fs.existsSync(metaPath)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const meta = createBundleMeta();
|
|
108
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
109
|
+
(0, runtime_1.emitNervesEvent)({
|
|
110
|
+
component: "mind",
|
|
111
|
+
event: "mind.bundle_meta_backfill",
|
|
112
|
+
message: "backfilled missing bundle-meta.json",
|
|
113
|
+
meta: { bundleRoot },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Non-blocking: if we can't write, that's okay
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Reset the backfill tracking set. Used in tests.
|
|
122
|
+
*/
|
|
123
|
+
function resetBackfillTracking() {
|
|
124
|
+
_backfilledRoots.clear();
|
|
125
|
+
}
|
|
56
126
|
const CANONICAL_FILE_PATHS = new Set(exports.CANONICAL_BUNDLE_MANIFEST
|
|
57
127
|
.filter((entry) => entry.kind === "file")
|
|
58
128
|
.map((entry) => entry.path));
|
package/dist/mind/context.js
CHANGED
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.trimMessages = trimMessages;
|
|
37
|
+
exports.validateSessionMessages = validateSessionMessages;
|
|
38
|
+
exports.repairSessionMessages = repairSessionMessages;
|
|
37
39
|
exports.saveSession = saveSession;
|
|
38
40
|
exports.loadSession = loadSession;
|
|
39
41
|
exports.postTurn = postTurn;
|
|
@@ -48,17 +50,17 @@ function buildTrimmableBlocks(messages) {
|
|
|
48
50
|
let i = 0;
|
|
49
51
|
while (i < messages.length) {
|
|
50
52
|
const msg = messages[i];
|
|
51
|
-
if (msg
|
|
53
|
+
if (msg.role === "system") {
|
|
52
54
|
i++;
|
|
53
55
|
continue;
|
|
54
56
|
}
|
|
55
57
|
// Tool coherence block: assistant message with tool_calls + immediately following tool results
|
|
56
|
-
if (msg
|
|
58
|
+
if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
57
59
|
const indices = [i];
|
|
58
60
|
i++;
|
|
59
61
|
while (i < messages.length) {
|
|
60
62
|
const next = messages[i];
|
|
61
|
-
if (next
|
|
63
|
+
if (next.role !== "tool")
|
|
62
64
|
break;
|
|
63
65
|
indices.push(i);
|
|
64
66
|
i++;
|
|
@@ -76,13 +78,13 @@ function buildTrimmableBlocks(messages) {
|
|
|
76
78
|
function getSystemMessageIndices(messages) {
|
|
77
79
|
const indices = [];
|
|
78
80
|
for (let i = 0; i < messages.length; i++) {
|
|
79
|
-
if (messages[i]
|
|
81
|
+
if (messages[i].role === "system")
|
|
80
82
|
indices.push(i);
|
|
81
83
|
}
|
|
82
84
|
return indices;
|
|
83
85
|
}
|
|
84
86
|
function buildTrimmedMessages(messages, kept) {
|
|
85
|
-
return messages.filter((m, idx) => m
|
|
87
|
+
return messages.filter((m, idx) => m.role === "system" || kept.has(idx));
|
|
86
88
|
}
|
|
87
89
|
function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
|
|
88
90
|
const targetTokens = Math.floor(maxTokens * (1 - contextMargin / 100));
|
|
@@ -130,7 +132,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
|
|
|
130
132
|
let remaining = actualTokenCount;
|
|
131
133
|
const kept = new Set();
|
|
132
134
|
for (let i = 0; i < messages.length; i++) {
|
|
133
|
-
if (messages[i]
|
|
135
|
+
if (messages[i].role !== "system")
|
|
134
136
|
kept.add(i);
|
|
135
137
|
}
|
|
136
138
|
// Drop oldest blocks until we fall under target.
|
|
@@ -144,7 +146,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
|
|
|
144
146
|
let trimmed = buildTrimmedMessages(messages, kept);
|
|
145
147
|
// If we're still above budget after dropping everything trimmable, preserve system only.
|
|
146
148
|
if (remaining > targetTokens) {
|
|
147
|
-
trimmed = messages.filter((m) => m
|
|
149
|
+
trimmed = messages.filter((m) => m.role === "system");
|
|
148
150
|
}
|
|
149
151
|
const estimatedAfter = (0, token_estimate_1.estimateTokensForMessages)(trimmed);
|
|
150
152
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -166,11 +168,116 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
|
|
|
166
168
|
});
|
|
167
169
|
return trimmed;
|
|
168
170
|
}
|
|
169
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Checks session invariant: after system messages, sequence must be
|
|
173
|
+
* user → assistant (with optional tool calls/results) → user → assistant...
|
|
174
|
+
* Never assistant → assistant without a user in between.
|
|
175
|
+
*/
|
|
176
|
+
function validateSessionMessages(messages) {
|
|
177
|
+
const violations = [];
|
|
178
|
+
let prevNonToolRole = null;
|
|
179
|
+
let prevAssistantHadToolCalls = false;
|
|
180
|
+
let sawToolResultSincePrevAssistant = false;
|
|
181
|
+
for (let i = 0; i < messages.length; i++) {
|
|
182
|
+
const msg = messages[i];
|
|
183
|
+
if (msg.role === "system")
|
|
184
|
+
continue;
|
|
185
|
+
if (msg.role === "tool") {
|
|
186
|
+
sawToolResultSincePrevAssistant = true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (msg.role === "assistant" && prevNonToolRole === "assistant") {
|
|
190
|
+
// assistant → tool(s) → assistant is valid (tool call flow)
|
|
191
|
+
if (!(prevAssistantHadToolCalls && sawToolResultSincePrevAssistant)) {
|
|
192
|
+
violations.push(`back-to-back assistant at index ${i}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
prevAssistantHadToolCalls = msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
|
|
196
|
+
sawToolResultSincePrevAssistant = false;
|
|
197
|
+
prevNonToolRole = msg.role;
|
|
198
|
+
}
|
|
199
|
+
return violations;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Repairs session invariant violations by merging consecutive assistant messages.
|
|
203
|
+
*/
|
|
204
|
+
function repairSessionMessages(messages) {
|
|
205
|
+
const violations = validateSessionMessages(messages);
|
|
206
|
+
if (violations.length === 0)
|
|
207
|
+
return messages;
|
|
208
|
+
const result = [];
|
|
209
|
+
for (const msg of messages) {
|
|
210
|
+
if (msg.role === "assistant" && result.length > 0) {
|
|
211
|
+
const prev = result[result.length - 1];
|
|
212
|
+
if (prev.role === "assistant" && !("tool_calls" in prev)) {
|
|
213
|
+
const prevContent = typeof prev.content === "string" ? prev.content : "";
|
|
214
|
+
const curContent = typeof msg.content === "string" ? msg.content : "";
|
|
215
|
+
prev.content = `${prevContent}\n\n${curContent}`;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
result.push(msg);
|
|
220
|
+
}
|
|
221
|
+
(0, runtime_1.emitNervesEvent)({
|
|
222
|
+
level: "info",
|
|
223
|
+
event: "mind.session_invariant_repair",
|
|
224
|
+
component: "mind",
|
|
225
|
+
message: "repaired session invariant violations",
|
|
226
|
+
meta: { violations },
|
|
227
|
+
});
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
function stripOrphanedToolResults(messages) {
|
|
231
|
+
const validCallIds = new Set();
|
|
232
|
+
for (const msg of messages) {
|
|
233
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
|
|
234
|
+
continue;
|
|
235
|
+
for (const toolCall of msg.tool_calls)
|
|
236
|
+
validCallIds.add(toolCall.id);
|
|
237
|
+
}
|
|
238
|
+
let removed = 0;
|
|
239
|
+
const repaired = messages.filter((msg) => {
|
|
240
|
+
if (msg.role !== "tool")
|
|
241
|
+
return true;
|
|
242
|
+
const keep = validCallIds.has(msg.tool_call_id);
|
|
243
|
+
if (!keep)
|
|
244
|
+
removed++;
|
|
245
|
+
return keep;
|
|
246
|
+
});
|
|
247
|
+
if (removed > 0) {
|
|
248
|
+
(0, runtime_1.emitNervesEvent)({
|
|
249
|
+
level: "info",
|
|
250
|
+
event: "mind.session_orphan_tool_result_repair",
|
|
251
|
+
component: "mind",
|
|
252
|
+
message: "removed orphaned tool results from session history",
|
|
253
|
+
meta: { removed },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return repaired;
|
|
257
|
+
}
|
|
258
|
+
function saveSession(filePath, messages, lastUsage, state) {
|
|
259
|
+
const violations = validateSessionMessages(messages);
|
|
260
|
+
if (violations.length > 0) {
|
|
261
|
+
(0, runtime_1.emitNervesEvent)({
|
|
262
|
+
level: "info",
|
|
263
|
+
event: "mind.session_invariant_violation",
|
|
264
|
+
component: "mind",
|
|
265
|
+
message: "session invariant violated on save",
|
|
266
|
+
meta: { path: filePath, violations },
|
|
267
|
+
});
|
|
268
|
+
messages = repairSessionMessages(messages);
|
|
269
|
+
}
|
|
270
|
+
messages = stripOrphanedToolResults(messages);
|
|
170
271
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
171
272
|
const envelope = { version: 1, messages };
|
|
172
273
|
if (lastUsage)
|
|
173
274
|
envelope.lastUsage = lastUsage;
|
|
275
|
+
if (state?.mustResolveBeforeHandoff === true || typeof state?.lastFriendActivityAt === "string") {
|
|
276
|
+
envelope.state = {
|
|
277
|
+
...(state?.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
|
|
278
|
+
...(typeof state?.lastFriendActivityAt === "string" ? { lastFriendActivityAt: state.lastFriendActivityAt } : {}),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
174
281
|
fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
|
|
175
282
|
}
|
|
176
283
|
function loadSession(filePath) {
|
|
@@ -179,13 +286,36 @@ function loadSession(filePath) {
|
|
|
179
286
|
const data = JSON.parse(raw);
|
|
180
287
|
if (data.version !== 1)
|
|
181
288
|
return null;
|
|
182
|
-
|
|
289
|
+
let messages = data.messages;
|
|
290
|
+
const violations = validateSessionMessages(messages);
|
|
291
|
+
if (violations.length > 0) {
|
|
292
|
+
(0, runtime_1.emitNervesEvent)({
|
|
293
|
+
level: "info",
|
|
294
|
+
event: "mind.session_invariant_violation",
|
|
295
|
+
component: "mind",
|
|
296
|
+
message: "session invariant violated on load",
|
|
297
|
+
meta: { path: filePath, violations },
|
|
298
|
+
});
|
|
299
|
+
messages = repairSessionMessages(messages);
|
|
300
|
+
}
|
|
301
|
+
messages = stripOrphanedToolResults(messages);
|
|
302
|
+
const rawState = data?.state && typeof data.state === "object" && data.state !== null
|
|
303
|
+
? data.state
|
|
304
|
+
: undefined;
|
|
305
|
+
const state = rawState && (rawState.mustResolveBeforeHandoff === true
|
|
306
|
+
|| typeof rawState.lastFriendActivityAt === "string")
|
|
307
|
+
? {
|
|
308
|
+
...(rawState.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
|
|
309
|
+
...(typeof rawState.lastFriendActivityAt === "string" ? { lastFriendActivityAt: rawState.lastFriendActivityAt } : {}),
|
|
310
|
+
}
|
|
311
|
+
: undefined;
|
|
312
|
+
return { messages, lastUsage: data.lastUsage, state };
|
|
183
313
|
}
|
|
184
314
|
catch {
|
|
185
315
|
return null;
|
|
186
316
|
}
|
|
187
317
|
}
|
|
188
|
-
function postTurn(messages, sessPath, usage, hooks) {
|
|
318
|
+
function postTurn(messages, sessPath, usage, hooks, state) {
|
|
189
319
|
if (hooks?.beforeTrim) {
|
|
190
320
|
try {
|
|
191
321
|
hooks.beforeTrim([...messages]);
|
|
@@ -205,7 +335,7 @@ function postTurn(messages, sessPath, usage, hooks) {
|
|
|
205
335
|
const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
|
|
206
336
|
const trimmed = trimMessages(messages, maxTokens, contextMargin, usage?.input_tokens);
|
|
207
337
|
messages.splice(0, messages.length, ...trimmed);
|
|
208
|
-
saveSession(sessPath, messages, usage);
|
|
338
|
+
saveSession(sessPath, messages, usage, state);
|
|
209
339
|
}
|
|
210
340
|
function deleteSession(filePath) {
|
|
211
341
|
try {
|
|
@@ -11,9 +11,22 @@ exports.ONBOARDING_TOKEN_THRESHOLD = 100_000;
|
|
|
11
11
|
function isOnboarding(friend) {
|
|
12
12
|
return (friend.totalTokens ?? 0) < exports.ONBOARDING_TOKEN_THRESHOLD;
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function hasLiveContinuityPressure(state) {
|
|
15
|
+
if (!state)
|
|
16
|
+
return false;
|
|
17
|
+
if (typeof state.currentObligation === "string" && state.currentObligation.trim().length > 0)
|
|
18
|
+
return true;
|
|
19
|
+
if (state.mustResolveBeforeHandoff === true)
|
|
20
|
+
return true;
|
|
21
|
+
if (state.hasQueuedFollowUp === true)
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
function getFirstImpressions(friend, state) {
|
|
15
26
|
if (!isOnboarding(friend))
|
|
16
27
|
return "";
|
|
28
|
+
if (hasLiveContinuityPressure(state))
|
|
29
|
+
return "";
|
|
17
30
|
(0, runtime_1.emitNervesEvent)({
|
|
18
31
|
component: "mind",
|
|
19
32
|
event: "mind.first_impressions",
|
|
@@ -37,7 +50,8 @@ function getFirstImpressions(friend) {
|
|
|
37
50
|
lines.push("- what do they do outside of work that they care about?");
|
|
38
51
|
lines.push("i don't ask all of these at once -- i weave them into conversation naturally, one or two at a time, and i genuinely follow up on what they share.");
|
|
39
52
|
lines.push("i introduce what i can do -- i have tools, integrations, and skills that can help them. i mention these naturally as they become relevant.");
|
|
40
|
-
lines.push("if
|
|
53
|
+
lines.push("if we're already in motion on a task, thread, or follow-up, i do not reset with a generic opener like 'hiya' or 'what do ya need help with?'. i continue directly or ask the specific next question.");
|
|
54
|
+
lines.push("only when the conversation is genuinely fresh and idle, with no active ask or thread in flight, a light opener is okay.");
|
|
41
55
|
lines.push("i save everything i learn immediately with save_friend_note -- names, roles, preferences, projects, anything. the bar is low: if i learned it, i save it.");
|
|
42
56
|
return lines.join("\n");
|
|
43
57
|
}
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
// Pure lookup, no I/O, cannot fail. Unknown channel gets minimal defaults.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.getChannelCapabilities = getChannelCapabilities;
|
|
6
|
+
exports.isRemoteChannel = isRemoteChannel;
|
|
7
|
+
exports.getAlwaysOnSenseNames = getAlwaysOnSenseNames;
|
|
6
8
|
const runtime_1 = require("../../nerves/runtime");
|
|
7
9
|
const CHANNEL_CAPABILITIES = {
|
|
8
10
|
cli: {
|
|
9
11
|
channel: "cli",
|
|
12
|
+
senseType: "local",
|
|
10
13
|
availableIntegrations: [],
|
|
11
14
|
supportsMarkdown: false,
|
|
12
15
|
supportsStreaming: true,
|
|
@@ -15,15 +18,35 @@ const CHANNEL_CAPABILITIES = {
|
|
|
15
18
|
},
|
|
16
19
|
teams: {
|
|
17
20
|
channel: "teams",
|
|
21
|
+
senseType: "closed",
|
|
18
22
|
availableIntegrations: ["ado", "graph", "github"],
|
|
19
23
|
supportsMarkdown: true,
|
|
20
24
|
supportsStreaming: true,
|
|
21
25
|
supportsRichCards: true,
|
|
22
26
|
maxMessageLength: Infinity,
|
|
23
27
|
},
|
|
28
|
+
bluebubbles: {
|
|
29
|
+
channel: "bluebubbles",
|
|
30
|
+
senseType: "open",
|
|
31
|
+
availableIntegrations: [],
|
|
32
|
+
supportsMarkdown: false,
|
|
33
|
+
supportsStreaming: false,
|
|
34
|
+
supportsRichCards: false,
|
|
35
|
+
maxMessageLength: Infinity,
|
|
36
|
+
},
|
|
37
|
+
inner: {
|
|
38
|
+
channel: "inner",
|
|
39
|
+
senseType: "internal",
|
|
40
|
+
availableIntegrations: [],
|
|
41
|
+
supportsMarkdown: false,
|
|
42
|
+
supportsStreaming: true,
|
|
43
|
+
supportsRichCards: false,
|
|
44
|
+
maxMessageLength: Infinity,
|
|
45
|
+
},
|
|
24
46
|
};
|
|
25
47
|
const DEFAULT_CAPABILITIES = {
|
|
26
48
|
channel: "cli",
|
|
49
|
+
senseType: "local",
|
|
27
50
|
availableIntegrations: [],
|
|
28
51
|
supportsMarkdown: false,
|
|
29
52
|
supportsStreaming: false,
|
|
@@ -39,3 +62,23 @@ function getChannelCapabilities(channel) {
|
|
|
39
62
|
});
|
|
40
63
|
return CHANNEL_CAPABILITIES[channel] ?? DEFAULT_CAPABILITIES;
|
|
41
64
|
}
|
|
65
|
+
/** Whether the channel is remote (open or closed) vs local/internal. */
|
|
66
|
+
function isRemoteChannel(capabilities) {
|
|
67
|
+
const senseType = capabilities?.senseType;
|
|
68
|
+
return senseType !== undefined && senseType !== "local" && senseType !== "internal";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Returns channel names whose senseType is "open" or "closed" -- i.e. channels
|
|
72
|
+
* that are always-on (daemon-managed) rather than interactive or internal.
|
|
73
|
+
*/
|
|
74
|
+
function getAlwaysOnSenseNames() {
|
|
75
|
+
(0, runtime_1.emitNervesEvent)({
|
|
76
|
+
component: "channels",
|
|
77
|
+
event: "channel.always_on_lookup",
|
|
78
|
+
message: "always-on sense names lookup",
|
|
79
|
+
meta: {},
|
|
80
|
+
});
|
|
81
|
+
return Object.entries(CHANNEL_CAPABILITIES)
|
|
82
|
+
.filter(([, cap]) => cap.senseType === "open" || cap.senseType === "closed")
|
|
83
|
+
.map(([channel]) => channel);
|
|
84
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.upsertGroupContextParticipants = upsertGroupContextParticipants;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
+
const CURRENT_SCHEMA_VERSION = 1;
|
|
7
|
+
function normalizeDisplayName(externalId, displayName) {
|
|
8
|
+
const trimmed = displayName?.trim();
|
|
9
|
+
return trimmed && trimmed.length > 0 ? trimmed : externalId;
|
|
10
|
+
}
|
|
11
|
+
function buildNameNotes(name, now) {
|
|
12
|
+
return name !== "Unknown"
|
|
13
|
+
? { name: { value: name, savedAt: now } }
|
|
14
|
+
: {};
|
|
15
|
+
}
|
|
16
|
+
function dedupeParticipants(participants) {
|
|
17
|
+
const deduped = new Map();
|
|
18
|
+
for (const participant of participants) {
|
|
19
|
+
const externalId = participant.externalId.trim();
|
|
20
|
+
if (!externalId)
|
|
21
|
+
continue;
|
|
22
|
+
const key = `${participant.provider}:${externalId}`;
|
|
23
|
+
if (!deduped.has(key)) {
|
|
24
|
+
deduped.set(key, {
|
|
25
|
+
...participant,
|
|
26
|
+
externalId,
|
|
27
|
+
displayName: participant.displayName?.trim() || undefined,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return Array.from(deduped.values());
|
|
32
|
+
}
|
|
33
|
+
function createGroupExternalId(provider, groupExternalId, linkedAt) {
|
|
34
|
+
return {
|
|
35
|
+
provider,
|
|
36
|
+
externalId: groupExternalId,
|
|
37
|
+
linkedAt,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function shouldPromoteToAcquaintance(friend) {
|
|
41
|
+
return (friend.trustLevel ?? "stranger") === "stranger";
|
|
42
|
+
}
|
|
43
|
+
function createAcquaintanceRecord(participant, groupExternalId, linkedAt) {
|
|
44
|
+
const name = normalizeDisplayName(participant.externalId, participant.displayName);
|
|
45
|
+
return {
|
|
46
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
47
|
+
name,
|
|
48
|
+
role: "acquaintance",
|
|
49
|
+
trustLevel: "acquaintance",
|
|
50
|
+
connections: [],
|
|
51
|
+
externalIds: [
|
|
52
|
+
{
|
|
53
|
+
provider: participant.provider,
|
|
54
|
+
externalId: participant.externalId,
|
|
55
|
+
linkedAt,
|
|
56
|
+
},
|
|
57
|
+
createGroupExternalId(participant.provider, groupExternalId, linkedAt),
|
|
58
|
+
],
|
|
59
|
+
tenantMemberships: [],
|
|
60
|
+
toolPreferences: {},
|
|
61
|
+
notes: buildNameNotes(name, linkedAt),
|
|
62
|
+
totalTokens: 0,
|
|
63
|
+
createdAt: linkedAt,
|
|
64
|
+
updatedAt: linkedAt,
|
|
65
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function upsertGroupContextParticipants(input) {
|
|
69
|
+
(0, runtime_1.emitNervesEvent)({
|
|
70
|
+
component: "friends",
|
|
71
|
+
event: "friends.group_context_upsert_start",
|
|
72
|
+
message: "upserting shared-group participant context",
|
|
73
|
+
meta: {
|
|
74
|
+
participantCount: input.participants.length,
|
|
75
|
+
hasGroupExternalId: input.groupExternalId.trim().length > 0,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const groupExternalId = input.groupExternalId.trim();
|
|
79
|
+
if (!groupExternalId) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const now = input.now ?? (() => new Date().toISOString());
|
|
83
|
+
const participants = dedupeParticipants(input.participants);
|
|
84
|
+
const results = [];
|
|
85
|
+
for (const participant of participants) {
|
|
86
|
+
const linkedAt = now();
|
|
87
|
+
const existing = await input.store.findByExternalId(participant.provider, participant.externalId);
|
|
88
|
+
if (!existing) {
|
|
89
|
+
const created = createAcquaintanceRecord(participant, groupExternalId, linkedAt);
|
|
90
|
+
await input.store.put(created.id, created);
|
|
91
|
+
results.push({
|
|
92
|
+
friendId: created.id,
|
|
93
|
+
name: created.name,
|
|
94
|
+
trustLevel: "acquaintance",
|
|
95
|
+
created: true,
|
|
96
|
+
updated: false,
|
|
97
|
+
addedGroupExternalId: true,
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const hasGroupExternalId = existing.externalIds.some((externalId) => externalId.externalId === groupExternalId);
|
|
102
|
+
const promoteToAcquaintance = shouldPromoteToAcquaintance(existing);
|
|
103
|
+
const trustLevel = promoteToAcquaintance
|
|
104
|
+
? "acquaintance"
|
|
105
|
+
: existing.trustLevel;
|
|
106
|
+
const role = promoteToAcquaintance
|
|
107
|
+
? "acquaintance"
|
|
108
|
+
: existing.role;
|
|
109
|
+
const updatedExternalIds = hasGroupExternalId
|
|
110
|
+
? existing.externalIds
|
|
111
|
+
: [...existing.externalIds, createGroupExternalId(participant.provider, groupExternalId, linkedAt)];
|
|
112
|
+
const updated = promoteToAcquaintance || !hasGroupExternalId;
|
|
113
|
+
const record = updated
|
|
114
|
+
? {
|
|
115
|
+
...existing,
|
|
116
|
+
role,
|
|
117
|
+
trustLevel,
|
|
118
|
+
externalIds: updatedExternalIds,
|
|
119
|
+
updatedAt: linkedAt,
|
|
120
|
+
}
|
|
121
|
+
: existing;
|
|
122
|
+
if (updated) {
|
|
123
|
+
await input.store.put(record.id, record);
|
|
124
|
+
}
|
|
125
|
+
results.push({
|
|
126
|
+
friendId: record.id,
|
|
127
|
+
name: record.name,
|
|
128
|
+
trustLevel,
|
|
129
|
+
created: false,
|
|
130
|
+
updated,
|
|
131
|
+
addedGroupExternalId: !hasGroupExternalId,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
(0, runtime_1.emitNervesEvent)({
|
|
135
|
+
component: "friends",
|
|
136
|
+
event: "friends.group_context_upsert_end",
|
|
137
|
+
message: "upserted shared-group participant context",
|
|
138
|
+
meta: {
|
|
139
|
+
participantCount: participants.length,
|
|
140
|
+
updatedCount: results.filter((result) => result.created || result.updated).length,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
@@ -100,6 +100,25 @@ class FileFriendStore {
|
|
|
100
100
|
}
|
|
101
101
|
return entries.some((entry) => entry.endsWith(".json"));
|
|
102
102
|
}
|
|
103
|
+
async listAll() {
|
|
104
|
+
let entries;
|
|
105
|
+
try {
|
|
106
|
+
entries = await fsPromises.readdir(this.friendsPath);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const records = [];
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (!entry.endsWith(".json"))
|
|
114
|
+
continue;
|
|
115
|
+
const raw = await this.readJson(path.join(this.friendsPath, entry));
|
|
116
|
+
if (!raw)
|
|
117
|
+
continue;
|
|
118
|
+
records.push(this.normalize(raw));
|
|
119
|
+
}
|
|
120
|
+
return records;
|
|
121
|
+
}
|
|
103
122
|
normalize(raw) {
|
|
104
123
|
const trustLevel = raw.trustLevel;
|
|
105
124
|
const normalizedTrustLevel = trustLevel === "family" ||
|