@awebai/oats 0.22.0
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/LICENSE +21 -0
- package/README.md +380 -0
- package/bin/oats.mjs +3294 -0
- package/capabilities/oats-authoring/oats.json +12 -0
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +297 -0
- package/capabilities/oats-aweb/injects/aweb.md +55 -0
- package/capabilities/oats-aweb/oats.json +47 -0
- package/capabilities/oats-aweb/package.json +20 -0
- package/capabilities/oats-jira/bin/oats-jira.mjs +40 -0
- package/capabilities/oats-jira/injects/jira.md +10 -0
- package/capabilities/oats-jira/oats.json +22 -0
- package/capabilities/oats-jira/package.json +25 -0
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +176 -0
- package/capabilities/oats-linear/README.md +234 -0
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +32 -0
- package/capabilities/oats-linear/bin/oats-linear.mjs +344 -0
- package/capabilities/oats-linear/injects/linear.md +8 -0
- package/capabilities/oats-linear/oats.json +24 -0
- package/capabilities/oats-linear/package.json +29 -0
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +217 -0
- package/capabilities/oats-linear/test/oats-linear.test.mjs +168 -0
- package/capabilities/oats-okf/agents/memory-harvest.md +25 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +283 -0
- package/capabilities/oats-okf/injects/okf.md +62 -0
- package/capabilities/oats-okf/oats.json +20 -0
- package/capabilities/oats-okf/package.json +22 -0
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +100 -0
- package/capabilities/oats-okf/skills/okf/SKILL.md +107 -0
- package/capabilities/oats-okf/skills/okf/scripts/okf-validate.mjs +123 -0
- package/capabilities/oats-review/agents/reviewer/AGENTS.md +53 -0
- package/capabilities/oats-review/agents/reviewer/soul.yaml +6 -0
- package/capabilities/oats-review/injects/review.md +68 -0
- package/capabilities/oats-review/oats.json +10 -0
- package/capabilities/oats-review/skills/code-review/SKILL.md +44 -0
- package/capabilities/oats-review/skills/security-review/SKILL.md +59 -0
- package/docs/capabilities.md +505 -0
- package/docs/capability-manifest.schema.json +223 -0
- package/docs/configuration.md +482 -0
- package/docs/conventions.md +73 -0
- package/docs/design/desktop-ux-plan.md +362 -0
- package/docs/design/package-engine-contract.md +813 -0
- package/docs/design/package-runtime-api.md +414 -0
- package/docs/desktop-cli-api.md +89 -0
- package/docs/desktop-succession.md +51 -0
- package/docs/desktop.md +187 -0
- package/docs/implementation.md +282 -0
- package/docs/integrations.md +123 -0
- package/docs/knowledge-theory.md +97 -0
- package/docs/knowledge.md +139 -0
- package/docs/layers.md +110 -0
- package/docs/migration-from-oas.md +122 -0
- package/docs/oats-config.schema.json +140 -0
- package/docs/oats-lock.schema.json +175 -0
- package/docs/oats-package.schema.json +129 -0
- package/docs/packages.md +442 -0
- package/docs/release-lane.md +132 -0
- package/docs/release-notes/v0.18.2.md +79 -0
- package/docs/release-notes/v0.18.3.md +63 -0
- package/docs/release-notes/v0.18.4.md +65 -0
- package/docs/release-notes/v0.18.5.md +66 -0
- package/docs/release-notes/v0.18.6.md +87 -0
- package/docs/release-notes/v0.19.0.md +186 -0
- package/docs/release-notes/v0.19.1.md +24 -0
- package/docs/release-notes/v0.19.2.md +32 -0
- package/docs/release-notes/v0.19.3.md +16 -0
- package/docs/release-notes/v0.19.4.md +12 -0
- package/docs/release-notes/v0.20.0.md +89 -0
- package/docs/release-notes/v0.22.0.md +180 -0
- package/docs/souls-and-instances.md +344 -0
- package/injects/framework-workspace.md +7 -0
- package/injects/instance-boundary.md +43 -0
- package/injects/local-soul.md +19 -0
- package/injects/oats.md +11 -0
- package/injects/work-attached.md +16 -0
- package/injects/work-checkout.md +12 -0
- package/injects/work-workspace.md +22 -0
- package/injects/work-worktree.md +13 -0
- package/lib/core.mjs +6792 -0
- package/lib/packages.mjs +1375 -0
- package/lib/tmux-config.mjs +35 -0
- package/package-catalog.json +44 -0
- package/package.json +55 -0
- package/packages/record/README.md +151 -0
- package/packages/record/bin/capture.mjs +256 -0
- package/packages/record/bin/recall.mjs +102 -0
- package/packages/record/bin/setup.mjs +281 -0
- package/packages/record/bin/turn-record.mjs +47 -0
- package/packages/record/docs/turn-record-sot.md +350 -0
- package/packages/record/lib/canonical.mjs +151 -0
- package/packages/record/lib/capture-aw.mjs +179 -0
- package/packages/record/lib/capture-cc.mjs +322 -0
- package/packages/record/lib/formats.mjs +296 -0
- package/packages/record/lib/ignore.mjs +155 -0
- package/packages/record/lib/index-db.mjs +524 -0
- package/packages/record/lib/project-aweb.mjs +251 -0
- package/packages/record/lib/segments.mjs +186 -0
- package/packages/record/lib/store.mjs +447 -0
- package/packages/record/lib/tags.mjs +114 -0
- package/packages/record/package.json +45 -0
- package/packages/record/test/vectors/README.md +34 -0
- package/packages/record/test/vectors/aweb-projection-v1.json +231 -0
- package/packages/record/test/vectors/journal-merge-v1.json +261 -0
- package/packages/record/test/vectors/turn-id-v1.json +152 -0
- package/packages/record/test/vectors/validate.mjs +391 -0
- package/skills/integration-authoring/SKILL.md +76 -0
- package/skills/oats/SKILL.md +151 -0
- package/skills/oats-config/SKILL.md +162 -0
- package/skills/oats-getting-started/SKILL.md +159 -0
- package/skills/oats-packages/SKILL.md +180 -0
- package/skills/oats-support/SKILL.md +79 -0
- package/skills/skill-craft/SKILL.md +109 -0
- package/skills/soul-craft/SKILL.md +109 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Projections of aweb messages into turns, per docs/turn-record-sot.md
|
|
2
|
+
// "Projection of aweb messages", plus the client-side aw log projections
|
|
3
|
+
// (~/.config/aw/logs/<account>.jsonl and .aw/interaction-log.jsonl).
|
|
4
|
+
//
|
|
5
|
+
// Every projection here is a pure function of its source data: the same
|
|
6
|
+
// source projected on any machine yields byte-identical canonical cores,
|
|
7
|
+
// hence identical ids, and union dedupes them.
|
|
8
|
+
|
|
9
|
+
import { CanonicalError, checkSignedConsistency, finishTurn } from "./canonical.mjs";
|
|
10
|
+
|
|
11
|
+
// ------------------------------------------------- server rows (signed)
|
|
12
|
+
|
|
13
|
+
// Project a signed message row (mail or chat). The row carries the
|
|
14
|
+
// canonical signed_payload string and signature byte-verbatim.
|
|
15
|
+
export function projectSignedRow(row, kind) {
|
|
16
|
+
if (kind !== "mail" && kind !== "chat") throw new CanonicalError(`bad kind ${kind}`);
|
|
17
|
+
const p = JSON.parse(row.signed_payload);
|
|
18
|
+
const core = {
|
|
19
|
+
v: 1,
|
|
20
|
+
ts: p.timestamp,
|
|
21
|
+
from: p.from,
|
|
22
|
+
to: p.to,
|
|
23
|
+
thread: "aweb:conv:" + row.conversation_id,
|
|
24
|
+
kind,
|
|
25
|
+
body: { subject: p.subject, text: p.body },
|
|
26
|
+
signature: row.signature,
|
|
27
|
+
signed_payload: row.signed_payload,
|
|
28
|
+
provenance: {
|
|
29
|
+
source: kind === "mail" ? "aweb-mail" : "aweb-chat",
|
|
30
|
+
fidelity: "projected",
|
|
31
|
+
origin: { message_id: row.message_id, conversation_id: row.conversation_id },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
return finishTurn(core);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Project a legacy unsigned mail row.
|
|
38
|
+
export function projectLegacyRow(row) {
|
|
39
|
+
const origin = {
|
|
40
|
+
message_id: row.message_id,
|
|
41
|
+
conversation_id: row.conversation_id,
|
|
42
|
+
};
|
|
43
|
+
for (const k of ["from_did", "from_stable_id", "to_did", "to_stable_id", "priority"]) {
|
|
44
|
+
if (row[k] !== undefined) origin[k] = row[k];
|
|
45
|
+
}
|
|
46
|
+
origin.timestamp_source = "created_at";
|
|
47
|
+
const core = {
|
|
48
|
+
v: 1,
|
|
49
|
+
ts: row.created_at,
|
|
50
|
+
from: row.from,
|
|
51
|
+
to: row.to,
|
|
52
|
+
thread: "aweb:conv:" + row.conversation_id,
|
|
53
|
+
kind: "mail",
|
|
54
|
+
body: { subject: row.subject, text: row.body },
|
|
55
|
+
provenance: { source: "aweb-mail", fidelity: "projected", origin },
|
|
56
|
+
};
|
|
57
|
+
return finishTurn(core);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Reconstruct the source row fields from a projected aweb turn.
|
|
61
|
+
// For signed turns, signed_payload is authoritative and its consistency
|
|
62
|
+
// with the turn is checked first.
|
|
63
|
+
export function unprojectRow(turn) {
|
|
64
|
+
const out = {};
|
|
65
|
+
if (turn.signed_payload !== undefined) {
|
|
66
|
+
checkSignedConsistency(turn);
|
|
67
|
+
const p = JSON.parse(turn.signed_payload);
|
|
68
|
+
Object.assign(out, p);
|
|
69
|
+
out.message_id = turn.provenance.origin.message_id;
|
|
70
|
+
out.conversation_id = turn.provenance.origin.conversation_id;
|
|
71
|
+
out.signature = turn.signature;
|
|
72
|
+
out.signed_payload = turn.signed_payload;
|
|
73
|
+
if (p.message_id !== undefined && p.message_id !== out.message_id) {
|
|
74
|
+
throw new CanonicalError("signed_payload message_id disagrees with origin");
|
|
75
|
+
}
|
|
76
|
+
if (p.conversation_id !== undefined && p.conversation_id !== out.conversation_id) {
|
|
77
|
+
throw new CanonicalError("signed_payload conversation_id disagrees with origin");
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
const origin = turn.provenance.origin;
|
|
82
|
+
out.message_id = origin.message_id;
|
|
83
|
+
out.conversation_id = origin.conversation_id;
|
|
84
|
+
for (const k of ["from_did", "from_stable_id", "to_did", "to_stable_id", "priority"]) {
|
|
85
|
+
if (origin[k] !== undefined) out[k] = origin[k];
|
|
86
|
+
}
|
|
87
|
+
out.from = turn.from;
|
|
88
|
+
out.to = turn.to;
|
|
89
|
+
out.subject = turn.body.subject;
|
|
90
|
+
out.body = turn.body.text;
|
|
91
|
+
if (origin.timestamp_source === "created_at") out.created_at = turn.ts;
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The canonical core forbids non-integer numbers (float serialization is
|
|
96
|
+
// not canonical across languages), but real log entries may carry them.
|
|
97
|
+
// Per the SOT, anything non-integral is carried as a string: floats are
|
|
98
|
+
// converted in place and their locations recorded so unprojection restores
|
|
99
|
+
// them exactly. Paths are '/'-joined key/index chains relative to origin,
|
|
100
|
+
// with JSON-Pointer escaping ("~" -> "~0", "/" -> "~1") so keys containing
|
|
101
|
+
// either character cannot corrupt the path.
|
|
102
|
+
function escapeKey(key) {
|
|
103
|
+
return String(key).replaceAll("~", "~0").replaceAll("/", "~1");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function unescapeKey(key) {
|
|
107
|
+
return key.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function stringifyFloats(value, path, paths) {
|
|
111
|
+
if (typeof value === "number" && !Number.isInteger(value)) {
|
|
112
|
+
paths.push(path);
|
|
113
|
+
return String(value);
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
return value.map((v, i) => stringifyFloats(v, `${path}/${i}`, paths));
|
|
117
|
+
}
|
|
118
|
+
if (value !== null && typeof value === "object") {
|
|
119
|
+
const out = {};
|
|
120
|
+
for (const [k, v] of Object.entries(value)) {
|
|
121
|
+
const seg = escapeKey(k);
|
|
122
|
+
out[k] = stringifyFloats(v, path ? `${path}/${seg}` : seg, paths);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Sanitize source fields for the canonical core and attach the projection
|
|
130
|
+
// markers under the reserved "~" key.
|
|
131
|
+
function sanitizeOrigin(origin, markers) {
|
|
132
|
+
const paths = [];
|
|
133
|
+
const out = stringifyFloats(origin, "", paths);
|
|
134
|
+
if (paths.length > 0) markers.float_paths = paths;
|
|
135
|
+
out["~"] = markers;
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function restoreFloats(target, paths) {
|
|
140
|
+
for (const path of paths ?? []) {
|
|
141
|
+
const keys = path.split("/").map(unescapeKey);
|
|
142
|
+
let node = target;
|
|
143
|
+
for (let i = 0; i < keys.length - 1 && node !== undefined && node !== null; i++) {
|
|
144
|
+
node = node[Array.isArray(node) ? Number(keys[i]) : keys[i]];
|
|
145
|
+
}
|
|
146
|
+
if (node === undefined || node === null) continue;
|
|
147
|
+
const last = Array.isArray(node) ? Number(keys[keys.length - 1]) : keys[keys.length - 1];
|
|
148
|
+
if (typeof node[last] === "string") node[last] = Number(node[last]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ------------------------------------------------- aw client comm log
|
|
153
|
+
|
|
154
|
+
// Fields lifted from a comm-log entry onto the turn itself; everything else
|
|
155
|
+
// (known or future) is preserved verbatim in provenance.origin, which makes
|
|
156
|
+
// the projection lossless by construction against schema drift.
|
|
157
|
+
const COMMLOG_LIFTED = new Set(["ts", "from", "to", "subject", "body", "ch", "conversation_id", "session_id"]);
|
|
158
|
+
|
|
159
|
+
// Projection bookkeeping lives under the single reserved origin key "~"
|
|
160
|
+
// (a key no real log schema plausibly uses), so a source field named
|
|
161
|
+
// "log", "account", or "float_paths" can never collide with a marker.
|
|
162
|
+
// A source field literally named "~" would still collide; accepted.
|
|
163
|
+
|
|
164
|
+
// Project one entry of ~/.config/aw/logs/<account>.jsonl. `selfName` names
|
|
165
|
+
// the account whose log this is; it becomes `from` on entries that omit it
|
|
166
|
+
// (the client logs its own sends without a from).
|
|
167
|
+
export function projectCommLogEntry(entry, { selfName }) {
|
|
168
|
+
const kind = entry.ch === "chat" ? "chat" : "mail";
|
|
169
|
+
const conversation = entry.conversation_id ?? entry.session_id;
|
|
170
|
+
const markers = { log: "commlog", account: selfName };
|
|
171
|
+
const origin = {};
|
|
172
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
173
|
+
if (!COMMLOG_LIFTED.has(k)) origin[k] = v;
|
|
174
|
+
}
|
|
175
|
+
if (entry.session_id !== undefined) markers.thread_key = "session_id";
|
|
176
|
+
if (entry.from === undefined) markers.from_omitted = true;
|
|
177
|
+
if (entry.body === undefined) markers.body_omitted = true;
|
|
178
|
+
const safeOrigin = sanitizeOrigin(origin, markers);
|
|
179
|
+
const core = {
|
|
180
|
+
v: 1,
|
|
181
|
+
ts: entry.ts,
|
|
182
|
+
from: entry.from ?? (entry.dir === "recv" ? "unknown" : selfName),
|
|
183
|
+
...(entry.to !== undefined ? { to: entry.to } : {}),
|
|
184
|
+
kind,
|
|
185
|
+
body: {
|
|
186
|
+
...(entry.subject !== undefined ? { subject: entry.subject } : {}),
|
|
187
|
+
text: entry.body ?? "",
|
|
188
|
+
},
|
|
189
|
+
provenance: { source: "aw-log", fidelity: "projected", origin: safeOrigin },
|
|
190
|
+
};
|
|
191
|
+
if (conversation !== undefined) core.thread = "aweb:conv:" + conversation;
|
|
192
|
+
return finishTurn(core);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function unprojectCommLogEntry(turn) {
|
|
196
|
+
const origin = turn.provenance.origin;
|
|
197
|
+
const markers = origin["~"] ?? {};
|
|
198
|
+
const entry = {};
|
|
199
|
+
for (const [k, v] of Object.entries(origin)) {
|
|
200
|
+
if (k === "~") continue;
|
|
201
|
+
entry[k] = structuredClone(v);
|
|
202
|
+
}
|
|
203
|
+
restoreFloats(entry, markers.float_paths);
|
|
204
|
+
entry.ts = turn.ts;
|
|
205
|
+
entry.ch = turn.kind;
|
|
206
|
+
if (!markers.from_omitted) entry.from = turn.from;
|
|
207
|
+
if (turn.to !== undefined) entry.to = turn.to;
|
|
208
|
+
if (turn.body.subject !== undefined) entry.subject = turn.body.subject;
|
|
209
|
+
if (!markers.body_omitted) entry.body = turn.body.text;
|
|
210
|
+
if (turn.thread !== undefined) {
|
|
211
|
+
const conversation = turn.thread.slice("aweb:conv:".length);
|
|
212
|
+
if (markers.thread_key === "session_id") entry.session_id = conversation;
|
|
213
|
+
else entry.conversation_id = conversation;
|
|
214
|
+
}
|
|
215
|
+
return entry;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ------------------------------------------------- .aw/interaction-log
|
|
219
|
+
|
|
220
|
+
// Entries look like {ts, kind: "mail_out"|"mail_in"|"chat_out"|..., message_id?,
|
|
221
|
+
// from?, to?, subject?, text}. Same lossless strategy: lift what maps onto
|
|
222
|
+
// the turn, preserve the rest in origin.
|
|
223
|
+
const INTERACTION_LIFTED = new Set(["ts", "kind", "from", "to", "subject", "text"]);
|
|
224
|
+
|
|
225
|
+
export function projectInteractionLogEntry(entry, { selfName, workspace }) {
|
|
226
|
+
const m = /^(mail|chat)_(out|in)$/.exec(entry.kind ?? "");
|
|
227
|
+
const kind = m ? m[1] : "note";
|
|
228
|
+
const dir = m ? (m[2] === "out" ? "send" : "recv") : undefined;
|
|
229
|
+
const markers = { log: "interaction", account: selfName, entry_kind: entry.kind };
|
|
230
|
+
if (workspace !== undefined) markers.workspace = workspace;
|
|
231
|
+
if (dir !== undefined) markers.dir = dir;
|
|
232
|
+
const origin = {};
|
|
233
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
234
|
+
if (!INTERACTION_LIFTED.has(k)) origin[k] = v;
|
|
235
|
+
}
|
|
236
|
+
if (entry.from === undefined) markers.from_omitted = true;
|
|
237
|
+
const safeOrigin = sanitizeOrigin(origin, markers);
|
|
238
|
+
const core = {
|
|
239
|
+
v: 1,
|
|
240
|
+
ts: entry.ts,
|
|
241
|
+
from: entry.from ?? (dir === "recv" ? "unknown" : selfName),
|
|
242
|
+
...(entry.to !== undefined ? { to: entry.to } : {}),
|
|
243
|
+
kind,
|
|
244
|
+
body: {
|
|
245
|
+
...(entry.subject !== undefined ? { subject: entry.subject } : {}),
|
|
246
|
+
text: entry.text ?? "",
|
|
247
|
+
},
|
|
248
|
+
provenance: { source: "aw-log", fidelity: "projected", origin: safeOrigin },
|
|
249
|
+
};
|
|
250
|
+
return finishTurn(core);
|
|
251
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Segment conventions — the reader's output, the unit of clothing.
|
|
2
|
+
//
|
|
3
|
+
// A piece of clothing is never a single turn: it is a segment, a
|
|
4
|
+
// contiguous stretch of conversation (tool calls and results included)
|
|
5
|
+
// that does one thing — an exploration, a design argument, an
|
|
6
|
+
// implementation. Segments are worn whole or not at all; dead-end
|
|
7
|
+
// outcomes are never worn (their lesson may be).
|
|
8
|
+
//
|
|
9
|
+
// A segment is recorded as a note turn. Its identity is (thread, start):
|
|
10
|
+
// a later note with the same identity revises it — extends the span,
|
|
11
|
+
// closes the outcome — and the latest (by the revising turn's ts, then
|
|
12
|
+
// id) wins.
|
|
13
|
+
//
|
|
14
|
+
// Spans are HALF-OPEN: start <= line < end. Adjacent segments meet at
|
|
15
|
+
// equality (seg1.end === seg2.start); endpoints are line numbers and need
|
|
16
|
+
// not land on entry-bearing lines. Spans reference immutable per-event
|
|
17
|
+
// session turns directly, so nothing needs pinning: line N of a session
|
|
18
|
+
// thread is the same turn forever.
|
|
19
|
+
|
|
20
|
+
import { finishTurn } from "./canonical.mjs";
|
|
21
|
+
|
|
22
|
+
// Type = ACTIVITY only (Juan's ruling): a misconceived implementation is
|
|
23
|
+
// still an implementation. Wrongness lives in outcome, never in type.
|
|
24
|
+
export const SEGMENT_TYPES = new Set([
|
|
25
|
+
"exploration",
|
|
26
|
+
"design",
|
|
27
|
+
"implementation",
|
|
28
|
+
"review",
|
|
29
|
+
"handoff",
|
|
30
|
+
"admin",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export const SEGMENT_OUTCOMES = new Set(["fruitful", "dead-end", "superseded", "ongoing"]);
|
|
34
|
+
|
|
35
|
+
export function segmentTurnCore({
|
|
36
|
+
owner,
|
|
37
|
+
thread,
|
|
38
|
+
start,
|
|
39
|
+
end = null,
|
|
40
|
+
type,
|
|
41
|
+
about = [],
|
|
42
|
+
established,
|
|
43
|
+
outcome,
|
|
44
|
+
lesson,
|
|
45
|
+
ts,
|
|
46
|
+
model,
|
|
47
|
+
}) {
|
|
48
|
+
if (!SEGMENT_TYPES.has(type)) throw new Error(`bad segment type ${type}`);
|
|
49
|
+
if (!SEGMENT_OUTCOMES.has(outcome)) throw new Error(`bad segment outcome ${outcome}`);
|
|
50
|
+
return {
|
|
51
|
+
v: 1,
|
|
52
|
+
ts,
|
|
53
|
+
from: owner,
|
|
54
|
+
kind: "note",
|
|
55
|
+
links: [{ rel: "segments", ref: thread }],
|
|
56
|
+
body: {
|
|
57
|
+
text: String(established ?? "").slice(0, 300),
|
|
58
|
+
segment: {
|
|
59
|
+
span: [start, ...(end ? [end] : [])],
|
|
60
|
+
type,
|
|
61
|
+
...(about.length > 0 ? { about } : {}),
|
|
62
|
+
established: String(established ?? ""),
|
|
63
|
+
outcome,
|
|
64
|
+
...(lesson ? { lesson: String(lesson).slice(0, 500) } : {}),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
provenance: {
|
|
68
|
+
source: "mind",
|
|
69
|
+
fidelity: "summary",
|
|
70
|
+
origin: { thread, ...(model ? { model } : {}) },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parseSegment(turn) {
|
|
76
|
+
const seg = turn?.body?.segment;
|
|
77
|
+
if (!seg || typeof seg !== "object" || !Array.isArray(seg.span) || seg.span.length < 1) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const links = Array.isArray(turn.links) ? turn.links : [];
|
|
81
|
+
const thread = links.find((l) => l?.rel === "segments")?.ref;
|
|
82
|
+
if (!thread) return null;
|
|
83
|
+
return {
|
|
84
|
+
thread,
|
|
85
|
+
start: String(seg.span[0]),
|
|
86
|
+
end: seg.span.length > 1 ? String(seg.span[1]) : null,
|
|
87
|
+
type: String(seg.type ?? ""),
|
|
88
|
+
about: Array.isArray(seg.about) ? seg.about : [],
|
|
89
|
+
established: String(seg.established ?? ""),
|
|
90
|
+
outcome: String(seg.outcome ?? "ongoing"),
|
|
91
|
+
lesson: typeof seg.lesson === "string" ? seg.lesson : null,
|
|
92
|
+
ts: turn.ts,
|
|
93
|
+
turnId: turn.id,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function tsMs(ts) {
|
|
98
|
+
const ms = Date.parse(String(ts));
|
|
99
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Resolve the current segment map for a thread from mind streams:
|
|
103
|
+
// group by (thread, start), latest revision wins (empty/unparseable
|
|
104
|
+
// timestamps compare as epoch so revision still resolves by turn id).
|
|
105
|
+
// Returns segments in start order (numeric when starts are line refs).
|
|
106
|
+
// v1 debt, accepted like taggedRefs: this scans the whole store
|
|
107
|
+
// (readAll + hiddenIds) on every call; cost grows with the record, not
|
|
108
|
+
// the thread. Index backing when the mind stream grows real volume.
|
|
109
|
+
export function segmentsFor(store, thread) {
|
|
110
|
+
const latest = new Map();
|
|
111
|
+
// Tombstoned segment notes are hidden like any other turn: readers of
|
|
112
|
+
// the map must not resurrect retracted judgments.
|
|
113
|
+
const byId = store.readAll();
|
|
114
|
+
const hidden = store.hiddenIds(byId);
|
|
115
|
+
for (const streamId of store.listStreams()) {
|
|
116
|
+
if (!streamId.includes("~mind")) continue;
|
|
117
|
+
for (const turn of store.readStream(streamId)) {
|
|
118
|
+
if (hidden.has(turn.id)) continue;
|
|
119
|
+
let seg;
|
|
120
|
+
try {
|
|
121
|
+
seg = parseSegment(turn);
|
|
122
|
+
} catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!seg || seg.thread !== thread) continue;
|
|
126
|
+
const key = seg.start;
|
|
127
|
+
const prev = latest.get(key);
|
|
128
|
+
if (!prev || tsMs(seg.ts) > tsMs(prev.ts) || (tsMs(seg.ts) === tsMs(prev.ts) && seg.turnId > prev.turnId)) {
|
|
129
|
+
latest.set(key, seg);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const locNum = (ref) => {
|
|
134
|
+
const m = /line:(\d+)/.exec(ref);
|
|
135
|
+
return m ? Number(m[1]) : Number.MAX_SAFE_INTEGER;
|
|
136
|
+
};
|
|
137
|
+
return [...latest.values()].sort((a, b) => locNum(a.start) - locNum(b.start));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------- spawns
|
|
141
|
+
|
|
142
|
+
// The creation mapping (Juan's requirement): when a new agent is dressed
|
|
143
|
+
// into existence, one note records agent id -> outfit -> segments, at the
|
|
144
|
+
// moment of creation. Quality assessment later is a join: the agent's own
|
|
145
|
+
// captured life and task outcomes attach to the same agent thread id.
|
|
146
|
+
export function spawnTurnCore({ owner, agentThread, outfit, task, harness, grant, ts, model }) {
|
|
147
|
+
return {
|
|
148
|
+
v: 1,
|
|
149
|
+
ts,
|
|
150
|
+
from: owner,
|
|
151
|
+
kind: "note",
|
|
152
|
+
links: [
|
|
153
|
+
{ rel: "spawned", ref: agentThread },
|
|
154
|
+
{ rel: "wore", ref: outfit },
|
|
155
|
+
],
|
|
156
|
+
body: {
|
|
157
|
+
text: `spawned ${agentThread} wearing ${outfit.slice(0, 24)}…`,
|
|
158
|
+
spawn: {
|
|
159
|
+
agent: agentThread,
|
|
160
|
+
outfit,
|
|
161
|
+
...(task ? { task: String(task) } : {}),
|
|
162
|
+
...(harness ? { harness } : {}),
|
|
163
|
+
...(grant ? { grant } : {}),
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
provenance: {
|
|
167
|
+
source: "mind",
|
|
168
|
+
fidelity: "projected",
|
|
169
|
+
origin: { ...(model ? { model } : {}) },
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function parseSpawn(turn) {
|
|
175
|
+
const spawn = turn?.body?.spawn;
|
|
176
|
+
if (!spawn || typeof spawn !== "object" || !spawn.agent || !spawn.outfit) return null;
|
|
177
|
+
return {
|
|
178
|
+
agent: String(spawn.agent),
|
|
179
|
+
outfit: String(spawn.outfit),
|
|
180
|
+
task: spawn.task ? String(spawn.task) : null,
|
|
181
|
+
harness: spawn.harness ? String(spawn.harness) : null,
|
|
182
|
+
grant: spawn.grant ? String(spawn.grant) : null,
|
|
183
|
+
ts: turn.ts,
|
|
184
|
+
turnId: turn.id,
|
|
185
|
+
};
|
|
186
|
+
}
|