@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +1 -1
  3. package/dist/api/owner-message-actions.d.ts +21 -0
  4. package/dist/api/owner-message-actions.d.ts.map +1 -0
  5. package/dist/api/owner-message-actions.js +100 -0
  6. package/dist/api/owner-message-actions.js.map +1 -0
  7. package/dist/api/viewer-server.d.ts.map +1 -1
  8. package/dist/api/viewer-server.js +1 -14
  9. package/dist/api/viewer-server.js.map +1 -1
  10. package/dist/api/wiki-actions.d.ts.map +1 -1
  11. package/dist/api/wiki-actions.js +5 -3
  12. package/dist/api/wiki-actions.js.map +1 -1
  13. package/dist/api/work-actions.d.ts +13 -4
  14. package/dist/api/work-actions.d.ts.map +1 -1
  15. package/dist/api/work-actions.js +336 -17
  16. package/dist/api/work-actions.js.map +1 -1
  17. package/dist/cli/commands/daemon.d.ts.map +1 -1
  18. package/dist/cli/commands/daemon.js +11 -1
  19. package/dist/cli/commands/daemon.js.map +1 -1
  20. package/dist/runtime/action-surface.d.ts +4 -1
  21. package/dist/runtime/action-surface.d.ts.map +1 -1
  22. package/dist/runtime/action-surface.js +6 -0
  23. package/dist/runtime/action-surface.js.map +1 -1
  24. package/dist/runtime/config.d.ts +2 -0
  25. package/dist/runtime/config.d.ts.map +1 -1
  26. package/dist/runtime/config.js +2 -1
  27. package/dist/runtime/config.js.map +1 -1
  28. package/dist/runtime/owner-runtime.d.ts.map +1 -1
  29. package/dist/runtime/owner-runtime.js +10 -1
  30. package/dist/runtime/owner-runtime.js.map +1 -1
  31. package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
  32. package/dist/runtime/owner-system-prompt.js +30 -4
  33. package/dist/runtime/owner-system-prompt.js.map +1 -1
  34. package/dist/runtime/record-orders.d.ts +32 -10
  35. package/dist/runtime/record-orders.d.ts.map +1 -1
  36. package/dist/runtime/record-orders.js +265 -59
  37. package/dist/runtime/record-orders.js.map +1 -1
  38. package/dist/runtime/report-scheduler.d.ts +8 -1
  39. package/dist/runtime/report-scheduler.d.ts.map +1 -1
  40. package/dist/runtime/report-scheduler.js +48 -17
  41. package/dist/runtime/report-scheduler.js.map +1 -1
  42. package/dist/runtime/session-start-context.d.ts +12 -0
  43. package/dist/runtime/session-start-context.d.ts.map +1 -1
  44. package/dist/runtime/session-start-context.js +22 -0
  45. package/dist/runtime/session-start-context.js.map +1 -1
  46. package/dist/runtime/stimulus-delivery.d.ts +5 -2
  47. package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
  48. package/dist/runtime/stimulus-delivery.js.map +1 -1
  49. package/dist/runtime/turn-orders.d.ts +17 -1
  50. package/dist/runtime/turn-orders.d.ts.map +1 -1
  51. package/dist/runtime/turn-orders.js +56 -14
  52. package/dist/runtime/turn-orders.js.map +1 -1
  53. package/package.json +2 -2
@@ -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
- if (typeof value === 'string' && /(?:Z|[+-]\d{2}:\d{2})$/.test(value)) {
90
- const parsed = Date.parse(value);
91
- if (Number.isFinite(parsed) && parsed >= 0)
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
- history: 'current',
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' && view !== 'items' && view !== 'detail' && view !== 'pipeline') {
586
- throw new Error('work.list view must be one of overview|items|detail|pipeline');
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 }) => workListCompact(item, now, score, ctx.timeZone)),
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