@jungjaehoon/mama-os 0.59.0 → 0.60.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/CHANGELOG.md +19 -1
- package/README.md +1 -1
- package/dist/api/owner-message-actions.d.ts +21 -0
- package/dist/api/owner-message-actions.d.ts.map +1 -0
- package/dist/api/owner-message-actions.js +100 -0
- package/dist/api/owner-message-actions.js.map +1 -0
- package/dist/api/viewer-server.d.ts.map +1 -1
- package/dist/api/viewer-server.js +1 -14
- package/dist/api/viewer-server.js.map +1 -1
- package/dist/api/wiki-actions.d.ts.map +1 -1
- package/dist/api/wiki-actions.js +5 -3
- package/dist/api/wiki-actions.js.map +1 -1
- package/dist/api/work-actions.d.ts +13 -4
- package/dist/api/work-actions.d.ts.map +1 -1
- package/dist/api/work-actions.js +336 -17
- package/dist/api/work-actions.js.map +1 -1
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +11 -1
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/runtime/action-surface.d.ts +4 -1
- package/dist/runtime/action-surface.d.ts.map +1 -1
- package/dist/runtime/action-surface.js +6 -0
- package/dist/runtime/action-surface.js.map +1 -1
- package/dist/runtime/config.d.ts +2 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +2 -1
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/owner-runtime.d.ts.map +1 -1
- package/dist/runtime/owner-runtime.js +10 -1
- package/dist/runtime/owner-runtime.js.map +1 -1
- package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
- package/dist/runtime/owner-system-prompt.js +30 -4
- package/dist/runtime/owner-system-prompt.js.map +1 -1
- package/dist/runtime/record-orders.d.ts +32 -10
- package/dist/runtime/record-orders.d.ts.map +1 -1
- package/dist/runtime/record-orders.js +265 -59
- package/dist/runtime/record-orders.js.map +1 -1
- package/dist/runtime/report-scheduler.d.ts +8 -1
- package/dist/runtime/report-scheduler.d.ts.map +1 -1
- package/dist/runtime/report-scheduler.js +48 -17
- package/dist/runtime/report-scheduler.js.map +1 -1
- package/dist/runtime/session-start-context.d.ts +12 -0
- package/dist/runtime/session-start-context.d.ts.map +1 -1
- package/dist/runtime/session-start-context.js +22 -0
- package/dist/runtime/session-start-context.js.map +1 -1
- package/dist/runtime/stimulus-delivery.d.ts +5 -2
- package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
- package/dist/runtime/stimulus-delivery.js.map +1 -1
- package/dist/runtime/turn-orders.d.ts +17 -1
- package/dist/runtime/turn-orders.d.ts.map +1 -1
- package/dist/runtime/turn-orders.js +56 -14
- package/dist/runtime/turn-orders.js.map +1 -1
- package/package.json +2 -2
package/dist/api/work-actions.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runTaskListView = void 0;
|
|
4
|
+
exports.offsetIsoTime = offsetIsoTime;
|
|
4
5
|
exports.runWorkListView = runWorkListView;
|
|
5
6
|
exports.workListActionRegistrations = workListActionRegistrations;
|
|
6
7
|
exports.assertReplayEventDatetime = assertReplayEventDatetime;
|
|
7
8
|
exports.minimalWorkActionRegistrations = minimalWorkActionRegistrations;
|
|
9
|
+
exports.workLinkRegistration = workLinkRegistration;
|
|
8
10
|
const node_crypto_1 = require("node:crypto");
|
|
9
11
|
const timezone_js_1 = require("../runtime/timezone.js");
|
|
10
12
|
const catalog_1 = require("@jungjaehoon/mama-core/api/catalog");
|
|
@@ -14,6 +16,8 @@ const WORK_LIST_DEFAULT_LIMIT = 25;
|
|
|
14
16
|
const WORK_LIST_MAX_LIMIT = 50;
|
|
15
17
|
const WORK_LIST_MAX_DETAIL_IDS = 4;
|
|
16
18
|
const WORK_LIST_DEFAULT_TEXT_LIMIT = 1_000;
|
|
19
|
+
const WORK_LIST_REVISION_SUMMARY_LIMIT = 300;
|
|
20
|
+
const WORK_LIST_EVENT_REVISIONS = 20;
|
|
17
21
|
const WORK_LIST_MAX_TEXT_LIMIT = 2_000;
|
|
18
22
|
const WORK_LIST_STATUSES = [
|
|
19
23
|
'pending',
|
|
@@ -86,15 +90,37 @@ function workListAsOf(value) {
|
|
|
86
90
|
* takes: on 2026-09-29 the agent passed "2026-09-29T00:00:00+09:00" and the call failed.
|
|
87
91
|
*/
|
|
88
92
|
function workListTime(value, field) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return parsed;
|
|
93
|
-
}
|
|
93
|
+
const parsed = offsetIsoTime(value);
|
|
94
|
+
if (parsed !== undefined)
|
|
95
|
+
return parsed;
|
|
94
96
|
if (Number.isSafeInteger(value) && value >= 0)
|
|
95
97
|
return value;
|
|
96
98
|
throw new Error(`work.list ${field} must be epoch milliseconds or an ISO time with its offset`);
|
|
97
99
|
}
|
|
100
|
+
const OFFSET_ISO_PATTERN = '^(\\d{4})-(\\d{2})-(\\d{2})[T ](\\d{2}):(\\d{2})(?::\\d{2}(?:\\.\\d{1,3})?)?(Z|([+-])(\\d{2}):(\\d{2}))$';
|
|
101
|
+
/** An ISO time that states its offset (Z or ±HH:MM), as epoch ms; undefined for anything else. */
|
|
102
|
+
function offsetIsoTime(value) {
|
|
103
|
+
const match = typeof value === 'string' ? new RegExp(OFFSET_ISO_PATTERN).exec(value) : null;
|
|
104
|
+
if (!match)
|
|
105
|
+
return undefined;
|
|
106
|
+
const parsed = Date.parse(match[0].replace(' ', 'T'));
|
|
107
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
108
|
+
return undefined;
|
|
109
|
+
// Date.parse rolls an impossible date or hour forward (Feb 30 becomes Mar 2): the wall time
|
|
110
|
+
// at the stated offset must read back as written.
|
|
111
|
+
const [, year, month, day, hour, minute, zone, sign, offsetHours, offsetMinutes] = match;
|
|
112
|
+
const offset = zone === 'Z' ? 0 : (sign === '-' ? -1 : 1) * (Number(offsetHours) * 60 + Number(offsetMinutes));
|
|
113
|
+
const wall = new Date(parsed + offset * 60_000);
|
|
114
|
+
const readBack = [
|
|
115
|
+
wall.getUTCFullYear(),
|
|
116
|
+
wall.getUTCMonth() + 1,
|
|
117
|
+
wall.getUTCDate(),
|
|
118
|
+
wall.getUTCHours(),
|
|
119
|
+
wall.getUTCMinutes(),
|
|
120
|
+
];
|
|
121
|
+
const written = [year, month, day, hour, minute].map(Number);
|
|
122
|
+
return readBack.every((part, index) => part === written[index]) ? parsed : undefined;
|
|
123
|
+
}
|
|
98
124
|
function workListFilter(input) {
|
|
99
125
|
const status = workListStatuses(input.status);
|
|
100
126
|
return {
|
|
@@ -110,8 +136,39 @@ function workListFilter(input) {
|
|
|
110
136
|
? {}
|
|
111
137
|
: { changedBefore: workListTime(input.changedBefore, 'changedBefore') }),
|
|
112
138
|
...(input.due === undefined ? {} : { due: workListDueFilter(input.due) }),
|
|
139
|
+
...(input.eventSince === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: { eventSince: workListTime(input.eventSince, 'eventSince') }),
|
|
142
|
+
...(input.eventBefore === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: { eventBefore: workListTime(input.eventBefore, 'eventBefore') }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** An item's revisions in the span, the newest 20 with the count: one item moved 51 times in a day. */
|
|
148
|
+
function workListEventPage(item, filter) {
|
|
149
|
+
const revisions = workListEventRevisions(item, filter);
|
|
150
|
+
return {
|
|
151
|
+
revisions: revisions.slice(-WORK_LIST_EVENT_REVISIONS),
|
|
152
|
+
revisionsTotal: revisions.length,
|
|
113
153
|
};
|
|
114
154
|
}
|
|
155
|
+
function workListEventBounded(filter) {
|
|
156
|
+
return filter.eventSince !== undefined || filter.eventBefore !== undefined;
|
|
157
|
+
}
|
|
158
|
+
/** The revisions in the event bounds; a revision with no event time counts at its write time. */
|
|
159
|
+
function workListEventRevisions(item, filter) {
|
|
160
|
+
return (item.chain ?? []).flatMap((entry) => {
|
|
161
|
+
const at = entry.eventDatetime ?? entry.createdAt;
|
|
162
|
+
if (filter.eventSince !== undefined && at < filter.eventSince)
|
|
163
|
+
return [];
|
|
164
|
+
if (filter.eventBefore !== undefined && at >= filter.eventBefore)
|
|
165
|
+
return [];
|
|
166
|
+
const summary = entry.summary !== null && entry.summary.length > WORK_LIST_REVISION_SUMMARY_LIMIT
|
|
167
|
+
? `${entry.summary.slice(0, WORK_LIST_REVISION_SUMMARY_LIMIT - 1)}…`
|
|
168
|
+
: entry.summary;
|
|
169
|
+
return [{ revision: entry.revision, at, status: entry.status, stage: entry.stage, summary }];
|
|
170
|
+
});
|
|
171
|
+
}
|
|
115
172
|
function workListDueFilter(value) {
|
|
116
173
|
if (typeof value !== 'string' || !WORK_LIST_DUE.includes(value)) {
|
|
117
174
|
throw new Error(`work.list due must be one of ${WORK_LIST_DUE.join('|')}`);
|
|
@@ -280,6 +337,8 @@ function workListMatches(item, filter, now, timeZone) {
|
|
|
280
337
|
return false;
|
|
281
338
|
if (filter.due !== undefined && workListDueGroup(item, now, timeZone) !== filter.due)
|
|
282
339
|
return false;
|
|
340
|
+
if (workListEventBounded(filter) && workListEventRevisions(item, filter).length === 0)
|
|
341
|
+
return false;
|
|
283
342
|
return true;
|
|
284
343
|
}
|
|
285
344
|
function workListDueGroup(item, now, timeZone) {
|
|
@@ -332,6 +391,8 @@ function workListFingerprint(filter, dueDay) {
|
|
|
332
391
|
filter.changedSince ?? null,
|
|
333
392
|
filter.changedBefore ?? null,
|
|
334
393
|
filter.due ?? null,
|
|
394
|
+
filter.eventSince ?? null,
|
|
395
|
+
filter.eventBefore ?? null,
|
|
335
396
|
]))
|
|
336
397
|
.digest('base64url')
|
|
337
398
|
.slice(0, 22);
|
|
@@ -355,7 +416,8 @@ function workListReadSnapshot(ctx, filter) {
|
|
|
355
416
|
let cursor;
|
|
356
417
|
for (;;) {
|
|
357
418
|
const query = {
|
|
358
|
-
|
|
419
|
+
// The chain carries each revision's event time and summary, read only for an event span.
|
|
420
|
+
history: workListEventBounded(filter) ? 'chain' : 'current',
|
|
359
421
|
limit: 100,
|
|
360
422
|
...(filter.asOf === undefined ? {} : { asOf: filter.asOf }),
|
|
361
423
|
...(cursor === undefined ? {} : { cursor }),
|
|
@@ -494,6 +556,94 @@ function workListDetailRecord(item, textOffset, textLimit, historyOffset, now, t
|
|
|
494
556
|
latestJudgmentRef: item.latestJudgmentRef,
|
|
495
557
|
};
|
|
496
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* One step of the walk: every link written from or to any revision of the item, with the relation,
|
|
561
|
+
* the reason, who wrote it, its evidence, any later correction, and the other end. A link sits on
|
|
562
|
+
* the revision that wrote it, so every revision is a seed; source citations (derived_from) and the
|
|
563
|
+
* item's own records are left out, and a correction shows on the link it corrects.
|
|
564
|
+
*/
|
|
565
|
+
function workListLinks(input, ctx) {
|
|
566
|
+
const ids = workListDetailIds(input.ids);
|
|
567
|
+
const items = [];
|
|
568
|
+
const missingIds = [];
|
|
569
|
+
const otherItems = new Map();
|
|
570
|
+
for (const id of ids) {
|
|
571
|
+
const page = ctx.knowledge.readWork({ history: 'all', ...(typeof id === 'number' ? { rowId: id } : { commitmentId: id }) }, ctx.access);
|
|
572
|
+
const item = page.items[0];
|
|
573
|
+
if (item === undefined || item.history === undefined) {
|
|
574
|
+
missingIds.push(id);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const own = new Set(item.history.map((revision) => revision.recordRef.id));
|
|
578
|
+
const graph = ctx.knowledge.queryGraph({
|
|
579
|
+
view: 'neighbors',
|
|
580
|
+
seeds: item.history.map((revision) => revision.recordRef),
|
|
581
|
+
maxDepth: 1,
|
|
582
|
+
direction: 'both',
|
|
583
|
+
relations: knowledge_1.TWIN_EDGE_TYPES.filter((relation) => relation !== 'derived_from'),
|
|
584
|
+
history: 'all',
|
|
585
|
+
limit: 500,
|
|
586
|
+
}, ctx.access);
|
|
587
|
+
const nodes = new Map(graph.nodes.map((node) => [`${node.ref.kind}:${node.ref.id}`, node]));
|
|
588
|
+
const links = [];
|
|
589
|
+
for (const edge of graph.edges) {
|
|
590
|
+
const out = own.has(edge.from.id);
|
|
591
|
+
if (!out && !own.has(edge.to.id))
|
|
592
|
+
continue;
|
|
593
|
+
const other = out ? edge.to : edge.from;
|
|
594
|
+
if (other.kind === 'edge' || (other.kind === 'memory' && own.has(other.id)))
|
|
595
|
+
continue;
|
|
596
|
+
const attrs = (edge.attrs ?? {});
|
|
597
|
+
const written = (attrs.relation_attrs ?? {});
|
|
598
|
+
const node = nodes.get(`${other.kind}:${other.id}`);
|
|
599
|
+
const data = node?.data;
|
|
600
|
+
const commitmentId = other.kind === 'memory' ? data?.work?.commitmentId : undefined;
|
|
601
|
+
if (commitmentId !== undefined && commitmentId === item.commitmentId)
|
|
602
|
+
continue;
|
|
603
|
+
const reason = typeof written.reason === 'string'
|
|
604
|
+
? written.reason
|
|
605
|
+
: typeof attrs.reason_text === 'string'
|
|
606
|
+
? attrs.reason_text
|
|
607
|
+
: null;
|
|
608
|
+
links.push({
|
|
609
|
+
edgeId: edge.id,
|
|
610
|
+
relation: edge.relation,
|
|
611
|
+
direction: out ? 'out' : 'in',
|
|
612
|
+
reason,
|
|
613
|
+
...(typeof written.role === 'string' ? { role: written.role } : {}),
|
|
614
|
+
source: attrs.source ?? null,
|
|
615
|
+
...(attrs.evidence_refs ? { evidenceRefs: attrs.evidence_refs } : {}),
|
|
616
|
+
...(attrs.corrected_by ? { correctedBy: attrs.corrected_by } : {}),
|
|
617
|
+
other: commitmentId !== undefined
|
|
618
|
+
? { kind: 'work', commitmentId }
|
|
619
|
+
: { kind: other.kind, id: other.id, name: node?.label ?? null },
|
|
620
|
+
});
|
|
621
|
+
if (commitmentId !== undefined)
|
|
622
|
+
otherItems.set(commitmentId, {});
|
|
623
|
+
}
|
|
624
|
+
items.push({
|
|
625
|
+
commitmentId: item.commitmentId,
|
|
626
|
+
title: workListText(workListValueObject(item.values).title),
|
|
627
|
+
revisions: item.history.length,
|
|
628
|
+
links,
|
|
629
|
+
coverage: graph.coverage,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
// The other items' current title and status, so the next step can be chosen without opening them.
|
|
633
|
+
for (const commitmentId of otherItems.keys()) {
|
|
634
|
+
const found = ctx.knowledge.readWork({ commitmentId }, ctx.access).items[0];
|
|
635
|
+
if (found !== undefined)
|
|
636
|
+
otherItems.set(commitmentId, {
|
|
637
|
+
title: workListText(workListValueObject(found.values).title),
|
|
638
|
+
status: workListStatus(found),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
for (const item of items)
|
|
642
|
+
for (const link of item.links)
|
|
643
|
+
if (link.other.kind === 'work')
|
|
644
|
+
Object.assign(link.other, otherItems.get(link.other.commitmentId) ?? {});
|
|
645
|
+
return { success: true, view: 'links', items, missingIds };
|
|
646
|
+
}
|
|
497
647
|
function workListDetail(input, ctx) {
|
|
498
648
|
const ids = workListDetailIds(input.ids);
|
|
499
649
|
const textOffset = workListNonNegativeInteger(input.text_offset, 'text_offset', 0);
|
|
@@ -582,14 +732,20 @@ function workListOverview(snapshot, rankedItems, now, timeZone) {
|
|
|
582
732
|
async function runWorkListView(rawInput, ctx) {
|
|
583
733
|
const input = workListObject(rawInput);
|
|
584
734
|
const view = input.view === undefined ? 'items' : input.view;
|
|
585
|
-
if (view !== 'overview' &&
|
|
586
|
-
|
|
735
|
+
if (view !== 'overview' &&
|
|
736
|
+
view !== 'items' &&
|
|
737
|
+
view !== 'detail' &&
|
|
738
|
+
view !== 'pipeline' &&
|
|
739
|
+
view !== 'links') {
|
|
740
|
+
throw new Error('work.list view must be one of overview|items|detail|pipeline|links');
|
|
587
741
|
}
|
|
588
|
-
if (input.ids !== undefined && view !== 'detail') {
|
|
589
|
-
throw new Error('work.list ids are only valid with view=detail');
|
|
742
|
+
if (input.ids !== undefined && view !== 'detail' && view !== 'links') {
|
|
743
|
+
throw new Error('work.list ids are only valid with view=detail or view=links');
|
|
590
744
|
}
|
|
591
745
|
if (view === 'detail')
|
|
592
746
|
return workListDetail(input, ctx);
|
|
747
|
+
if (view === 'links')
|
|
748
|
+
return workListLinks(input, ctx);
|
|
593
749
|
const requested = workListFilter(input);
|
|
594
750
|
const filter = input.cursor !== undefined && Object.keys(requested).length === 0
|
|
595
751
|
? (workListCursorQuery(input.cursor) ?? requested)
|
|
@@ -657,7 +813,10 @@ async function runWorkListView(rawInput, ctx) {
|
|
|
657
813
|
return {
|
|
658
814
|
success: true,
|
|
659
815
|
view: 'items',
|
|
660
|
-
tasks: page.map(({ item, score }) =>
|
|
816
|
+
tasks: page.map(({ item, score }) => ({
|
|
817
|
+
...workListCompact(item, now, score, ctx.timeZone),
|
|
818
|
+
...(workListEventBounded(filter) ? workListEventPage(item, filter) : {}),
|
|
819
|
+
})),
|
|
661
820
|
total: rankedItems.length,
|
|
662
821
|
returned: page.length,
|
|
663
822
|
nextCursor: nextOffset === null
|
|
@@ -680,12 +839,12 @@ function workListActionRegistrations(ports) {
|
|
|
680
839
|
{
|
|
681
840
|
contract: {
|
|
682
841
|
name: 'work.list',
|
|
683
|
-
summary: 'Find owner work by what the turn needs: items filtered by status, stage, project, due, changedSince or changedBefore and ranked by text, in pages of 25 (50 max); overview counts; detail for up to 4 ids with the current record, its 20 newest evidence refs and its 5 newest revisions (history_offset pages older ones). pipeline returns every open item grouped by stage (rows in the order of its fields: commitmentId, title, status, assignee, deadline, latest_change in epoch seconds, latest_event) and ignores limit: read it inside a script that builds the board, not into your context.',
|
|
842
|
+
summary: 'Find owner work by what the turn needs: items filtered by status, stage, project, due, changedSince or changedBefore and ranked by text, in pages of 25 (50 max); overview counts; detail for up to 4 ids with the current record, its 20 newest evidence refs and its 5 newest revisions (history_offset pages older ones). eventSince and eventBefore find what happened in a span by source event time, each item with its revisions there. pipeline returns every open item grouped by stage (rows in the order of its fields: commitmentId, title, status, assignee, deadline, latest_change in epoch seconds, latest_event) and ignores limit: read it inside a script that builds the board, not into your context. links for up to 4 ids returns every link from or to the item with its relation, reason, writer, evidence, correction and the other end (item title and status, person, record or observation); follow it to the other item with detail.',
|
|
684
843
|
inputSchema: {
|
|
685
844
|
type: 'object',
|
|
686
845
|
additionalProperties: false,
|
|
687
846
|
properties: {
|
|
688
|
-
view: { type: 'string', enum: ['overview', 'items', 'detail', 'pipeline'] },
|
|
847
|
+
view: { type: 'string', enum: ['overview', 'items', 'detail', 'pipeline', 'links'] },
|
|
689
848
|
ids: {
|
|
690
849
|
type: 'array',
|
|
691
850
|
minItems: 1,
|
|
@@ -733,6 +892,20 @@ function workListActionRegistrations(ports) {
|
|
|
733
892
|
enum: WORK_LIST_DUE,
|
|
734
893
|
description: "Open items whose deadline is past, is the owner's today, is later, or is unset.",
|
|
735
894
|
},
|
|
895
|
+
eventSince: {
|
|
896
|
+
oneOf: [
|
|
897
|
+
{ type: 'integer', minimum: 0 },
|
|
898
|
+
{ type: 'string', minLength: 1 },
|
|
899
|
+
],
|
|
900
|
+
description: 'Only items with a revision whose source event time is at or after this time (epoch ms or ISO with offset); each item lists those revisions with their summaries.',
|
|
901
|
+
},
|
|
902
|
+
eventBefore: {
|
|
903
|
+
oneOf: [
|
|
904
|
+
{ type: 'integer', minimum: 0 },
|
|
905
|
+
{ type: 'string', minLength: 1 },
|
|
906
|
+
],
|
|
907
|
+
description: 'Only items with a revision whose source event time is before this time; with eventSince, what happened on a day.',
|
|
908
|
+
},
|
|
736
909
|
text_offset: { type: 'integer', minimum: 0 },
|
|
737
910
|
text_limit: { type: 'integer', minimum: 1, maximum: WORK_LIST_MAX_TEXT_LIMIT },
|
|
738
911
|
history_offset: {
|
|
@@ -883,8 +1056,8 @@ const commandFields = {
|
|
|
883
1056
|
items: catalog_1.recordLinkSchema,
|
|
884
1057
|
},
|
|
885
1058
|
eventDatetime: {
|
|
886
|
-
description: 'Source event time as epoch milliseconds, not replay time, or null, e.g. 1760000000000.',
|
|
887
|
-
oneOf: [{ type: 'number' }, { type: 'null' }],
|
|
1059
|
+
description: 'Source event time as epoch milliseconds or an ISO time with its offset, not replay time, or null, e.g. 1760000000000 or "2026-01-01T09:30:00+09:00".',
|
|
1060
|
+
oneOf: [{ type: 'number' }, { type: 'string', pattern: OFFSET_ISO_PATTERN }, { type: 'null' }],
|
|
888
1061
|
},
|
|
889
1062
|
recordedAt: {
|
|
890
1063
|
type: 'number',
|
|
@@ -948,6 +1121,18 @@ function assertSourceRefsExist(body, ports) {
|
|
|
948
1121
|
}
|
|
949
1122
|
}
|
|
950
1123
|
}
|
|
1124
|
+
/**
|
|
1125
|
+
* The ledger stores eventDatetime as epoch ms. On 2026-09-29 an agent passed the source time as
|
|
1126
|
+
* "…T12:30:00+09:00", the write was refused, and the record order ended without a record.
|
|
1127
|
+
*/
|
|
1128
|
+
function withEventEpoch(body, action) {
|
|
1129
|
+
if (typeof body.eventDatetime !== 'string')
|
|
1130
|
+
return body;
|
|
1131
|
+
const eventDatetime = offsetIsoTime(body.eventDatetime);
|
|
1132
|
+
if (eventDatetime === undefined)
|
|
1133
|
+
throw new knowledge_1.JudgmentError('INVALID_COMMAND', `${action} eventDatetime must be epoch milliseconds or an ISO time with its offset`);
|
|
1134
|
+
return { ...body, eventDatetime };
|
|
1135
|
+
}
|
|
951
1136
|
function commandFieldsFrom(body) {
|
|
952
1137
|
const { commitmentId: _commitmentId, expectedRevision: _expectedRevision, ...fields } = body;
|
|
953
1138
|
return fields;
|
|
@@ -989,7 +1174,7 @@ function minimalWorkActionRegistrations(ports) {
|
|
|
989
1174
|
],
|
|
990
1175
|
},
|
|
991
1176
|
exec: (input, context) => {
|
|
992
|
-
const body = input;
|
|
1177
|
+
const body = withEventEpoch(input, 'work.create');
|
|
993
1178
|
assertReplayEventDatetime(body, context, 'work.create');
|
|
994
1179
|
assertSourceRefsExist(body, ports);
|
|
995
1180
|
return ports.knowledge.createWork({
|
|
@@ -1019,7 +1204,7 @@ function minimalWorkActionRegistrations(ports) {
|
|
|
1019
1204
|
],
|
|
1020
1205
|
},
|
|
1021
1206
|
exec: (input, context) => {
|
|
1022
|
-
const body = input;
|
|
1207
|
+
const body = withEventEpoch(input, 'work.revise');
|
|
1023
1208
|
assertReplayEventDatetime(body, context, 'work.revise');
|
|
1024
1209
|
assertSourceRefsExist(body, ports);
|
|
1025
1210
|
const commitmentId = body.commitmentId;
|
|
@@ -1034,6 +1219,140 @@ function minimalWorkActionRegistrations(ports) {
|
|
|
1034
1219
|
}, context.access);
|
|
1035
1220
|
},
|
|
1036
1221
|
},
|
|
1222
|
+
workLinkRegistration(ports),
|
|
1037
1223
|
];
|
|
1038
1224
|
}
|
|
1225
|
+
// Replacing and amending change the target's state and are written with a revision (work.revise
|
|
1226
|
+
// links, memory.retire), not as a bare link.
|
|
1227
|
+
const LINK_RELATIONS = [
|
|
1228
|
+
'builds_on',
|
|
1229
|
+
'refines',
|
|
1230
|
+
'contradicts',
|
|
1231
|
+
'debates',
|
|
1232
|
+
'synthesizes',
|
|
1233
|
+
'mentions',
|
|
1234
|
+
'blocks',
|
|
1235
|
+
'next_action_for',
|
|
1236
|
+
];
|
|
1237
|
+
const linkSchema = {
|
|
1238
|
+
type: 'object',
|
|
1239
|
+
additionalProperties: false,
|
|
1240
|
+
required: ['from', 'to', 'relation', 'reason'],
|
|
1241
|
+
properties: {
|
|
1242
|
+
from: {
|
|
1243
|
+
type: 'string',
|
|
1244
|
+
description: 'commitmentId of the item the link is stated from, e.g. "commitment_123".',
|
|
1245
|
+
},
|
|
1246
|
+
to: {
|
|
1247
|
+
type: 'object',
|
|
1248
|
+
additionalProperties: false,
|
|
1249
|
+
required: ['kind', 'id'],
|
|
1250
|
+
properties: {
|
|
1251
|
+
kind: {
|
|
1252
|
+
type: 'string',
|
|
1253
|
+
enum: ['work', 'memory', 'registry', 'observation', 'edge'],
|
|
1254
|
+
description: 'What the other end is, e.g. "work".',
|
|
1255
|
+
},
|
|
1256
|
+
id: {
|
|
1257
|
+
type: 'string',
|
|
1258
|
+
minLength: 1,
|
|
1259
|
+
description: 'Its id: a commitmentId, record id, node id, observationRef or edgeId.',
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
description: 'The other end: an item by commitmentId (kind work), one record, a person node, an observation, or a link (kind edge) the new link contradicts, e.g. {"kind":"work","id":"commitment_456"}.',
|
|
1263
|
+
},
|
|
1264
|
+
relation: {
|
|
1265
|
+
type: 'string',
|
|
1266
|
+
enum: [...LINK_RELATIONS],
|
|
1267
|
+
description: 'builds_on for an earlier case or work this continues, refines for a correction, contradicts for a reversal or a wrong link, e.g. "builds_on".',
|
|
1268
|
+
},
|
|
1269
|
+
reason: {
|
|
1270
|
+
type: 'string',
|
|
1271
|
+
minLength: 1,
|
|
1272
|
+
description: 'What relates the two, in a sentence the next reader can check, e.g. "The same setup problem; it ended in a client FIX after the setup was redone".',
|
|
1273
|
+
},
|
|
1274
|
+
evidenceRefs: {
|
|
1275
|
+
type: 'array',
|
|
1276
|
+
items: { type: 'string', minLength: 1 },
|
|
1277
|
+
description: 'observationRefs the reason rests on, e.g. ["obs_123"].',
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
function headRecord(ports, commitmentId, access) {
|
|
1282
|
+
const item = ports.knowledge.readWork({ commitmentId }, access).items[0];
|
|
1283
|
+
if (item === undefined)
|
|
1284
|
+
throw new knowledge_1.JudgmentError('REFERENCE_NOT_FOUND', `Commitment is unavailable: ${commitmentId}`);
|
|
1285
|
+
return { kind: 'memory', id: item.latestJudgmentRef.id };
|
|
1286
|
+
}
|
|
1287
|
+
function workLinkRegistration(ports) {
|
|
1288
|
+
return {
|
|
1289
|
+
contract: {
|
|
1290
|
+
name: 'work.link',
|
|
1291
|
+
recallableWrite: true,
|
|
1292
|
+
summary: 'Link one work item to another item, a record, a person or an observation, with the reason you judged; it appends an edge and writes no revision. A link to a link (to.kind edge, relation contradicts) corrects it: the wrong link stays in the history with the correction beside it.',
|
|
1293
|
+
inputSchema: linkSchema,
|
|
1294
|
+
examples: [
|
|
1295
|
+
{
|
|
1296
|
+
title: 'Link an earlier case of the same kind',
|
|
1297
|
+
input: {
|
|
1298
|
+
from: 'commitment-reference',
|
|
1299
|
+
to: { kind: 'work', id: 'commitment-earlier' },
|
|
1300
|
+
relation: 'builds_on',
|
|
1301
|
+
reason: 'what is the same and how the earlier one ended',
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1305
|
+
},
|
|
1306
|
+
exec: (input, context) => {
|
|
1307
|
+
const body = input;
|
|
1308
|
+
for (const ref of body.evidenceRefs ?? []) {
|
|
1309
|
+
if (!ports.observationExists(ref))
|
|
1310
|
+
throw new knowledge_1.JudgmentError('REFERENCE_NOT_FOUND', `evidenceRefs names an unavailable observation: ${ref}`);
|
|
1311
|
+
}
|
|
1312
|
+
// A retried call finds its link before the heads are read again: the item may have been
|
|
1313
|
+
// revised since, and the retry must still return the same link. The ends are compared as
|
|
1314
|
+
// revisions of the named items; core's content hash compares the rest of the statement.
|
|
1315
|
+
const commandId = operationId(context, 'work.link');
|
|
1316
|
+
const earlier = ports.knowledge.findLink(commandId, context.access);
|
|
1317
|
+
let from;
|
|
1318
|
+
let to;
|
|
1319
|
+
if (earlier) {
|
|
1320
|
+
const isRevisionOf = (ref, commitmentId) => ref.kind === 'memory' &&
|
|
1321
|
+
(ports.knowledge.readWork({ commitmentId, history: 'all' }, context.access).items[0]
|
|
1322
|
+
?.history ?? []).some((revision) => revision.recordRef.id === ref.id);
|
|
1323
|
+
const sameTarget = body.to.kind === 'work'
|
|
1324
|
+
? isRevisionOf(earlier.to, body.to.id)
|
|
1325
|
+
: earlier.to.kind === body.to.kind && earlier.to.id === body.to.id;
|
|
1326
|
+
if (!isRevisionOf(earlier.from, body.from) || !sameTarget)
|
|
1327
|
+
throw new knowledge_1.JudgmentError('COMMAND_CONFLICT', 'The command id is already bound to another link');
|
|
1328
|
+
from = earlier.from;
|
|
1329
|
+
to = earlier.to;
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
from = headRecord(ports, body.from, context.access);
|
|
1333
|
+
to =
|
|
1334
|
+
body.to.kind === 'work'
|
|
1335
|
+
? headRecord(ports, body.to.id, context.access)
|
|
1336
|
+
: { kind: body.to.kind, id: body.to.id };
|
|
1337
|
+
}
|
|
1338
|
+
const receipt = ports.knowledge.appendLink({
|
|
1339
|
+
commandId,
|
|
1340
|
+
from,
|
|
1341
|
+
to,
|
|
1342
|
+
relation: body.relation,
|
|
1343
|
+
reason: body.reason,
|
|
1344
|
+
...(body.evidenceRefs?.length
|
|
1345
|
+
? {
|
|
1346
|
+
evidenceRefs: body.evidenceRefs.map((id) => ({
|
|
1347
|
+
kind: 'observation',
|
|
1348
|
+
id,
|
|
1349
|
+
})),
|
|
1350
|
+
}
|
|
1351
|
+
: {}),
|
|
1352
|
+
modelRunId: context.session?.modelRunId ?? null,
|
|
1353
|
+
}, context.access);
|
|
1354
|
+
return { ...receipt, from: { commitmentId: body.from, recordRef: from }, to };
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1039
1358
|
//# sourceMappingURL=work-actions.js.map
|