@ghostpaw/affinity 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -3,7 +3,7 @@ import { dirname, join, resolve } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
 
5
5
  // src/version.ts
6
- var affinityVersion = "0.6.0";
6
+ var affinityVersion = "0.6.1";
7
7
 
8
8
  // src/surface/affinity_mcp_instructions.ts
9
9
  var affinityMcpInstructions = "Affinity is a local relationship model and social CRM. Search before creating contacts. Inspect before revising or merging. Record direct evidence honestly, keep identities and structural ties distinct from social links, and ask for clarification when a target is ambiguous. Review commitments and recurring dates regularly. Derived scores guide attention and do not prove relationship quality.";
@@ -380,6 +380,8 @@ function mapEventRowToEventRecord(row, participants) {
380
380
  momentKind: row.moment_kind,
381
381
  participants
382
382
  };
383
+ if (row.provenance_json != null)
384
+ base.provenance = JSON.parse(row.provenance_json);
383
385
  if (row.content != null) base.content = decodeEventText(row.content);
384
386
  if (row.channel != null) base.channel = row.channel;
385
387
  if (row.external_source != null && row.external_id != null)
@@ -670,14 +672,9 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
670
672
  text = text.replace(" ", "T");
671
673
  if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
672
674
  }
673
- let rounding = 0;
674
675
  text = text.replace(
675
676
  /\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
676
- (_, digits) => {
677
- const ms = Math.round(Number(`0.${digits}`) * 1e3);
678
- rounding = ms === 1e3 ? 1 : 0;
679
- return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
680
- }
677
+ (_, digits) => `.${digits.slice(0, 3)}`
681
678
  );
682
679
  if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
683
680
  const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
@@ -697,7 +694,7 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
697
694
  const midnight = Date.parse(`${day}T00:00:00Z`);
698
695
  if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
699
696
  return fail();
700
- const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text) + rounding;
697
+ const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
701
698
  if (!Number.isFinite(timestamp)) return fail();
702
699
  return { timestamp, day };
703
700
  }
@@ -1737,7 +1734,7 @@ var externalRefSchema = objectSchema(
1737
1734
  );
1738
1735
  var eventMetadataSchema = {
1739
1736
  affectsBond: booleanSchema(
1740
- "Whether evidence affects the relationship; defaults false for observations with significance <=2."
1737
+ "Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
1741
1738
  ),
1742
1739
  content: stringSchema(
1743
1740
  "Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
@@ -2287,6 +2284,13 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
2287
2284
  `${path}.${key}`,
2288
2285
  referenceNow
2289
2286
  );
2287
+ if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
2288
+ const provenance = object.provenance;
2289
+ object.provenance = {
2290
+ ...provenance && typeof provenance === "object" ? provenance : {},
2291
+ [`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
2292
+ };
2293
+ }
2290
2294
  object[key] = parsed.timestamp;
2291
2295
  if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
2292
2296
  object[key.replace(/At$/, "On")] ??= parsed.day;
@@ -2304,7 +2308,7 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
2304
2308
  const error = validateToolInput(
2305
2309
  child,
2306
2310
  object[key],
2307
- `${path}.${key}`,
2311
+ key === "input" && path === "input" ? path : `${path}.${key}`,
2308
2312
  referenceNow,
2309
2313
  strict
2310
2314
  );
@@ -2741,6 +2745,11 @@ function setAttribute(db, target, name, value) {
2741
2745
  const stored = normalizeAttributeValue(value);
2742
2746
  const now = resolveNow();
2743
2747
  const existing = getLiveAttributeRow(db, target, key);
2748
+ if (existing?.value === stored)
2749
+ return buildAttributeMutationReceipt(
2750
+ mapAttributeRowToAttributeRecord(existing),
2751
+ {}
2752
+ );
2744
2753
  if (existing) {
2745
2754
  db.prepare(
2746
2755
  "UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
@@ -2880,7 +2889,30 @@ function linkLocatorSchema2(description) {
2880
2889
  };
2881
2890
  }
2882
2891
  function resolveAttributeTarget(db, target) {
2883
- if (target.kind === "event") return { kind: "ok", target };
2892
+ if (target.kind === "event") {
2893
+ if (target.eventId === void 0 === (target.ref === void 0))
2894
+ return {
2895
+ kind: "error",
2896
+ result: toolFailure(
2897
+ "protocol",
2898
+ "invalid_input",
2899
+ "Invalid event target.",
2900
+ "Provide exactly one of target.eventId or target.ref."
2901
+ )
2902
+ };
2903
+ const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
2904
+ if (eventId === void 0)
2905
+ return {
2906
+ kind: "error",
2907
+ result: toolFailure(
2908
+ "domain",
2909
+ "not_found",
2910
+ "Event not found.",
2911
+ "No event matches target.ref."
2912
+ )
2913
+ };
2914
+ return { kind: "ok", target: { kind: "event", eventId } };
2915
+ }
2884
2916
  if (target.kind === "contact") {
2885
2917
  const contact = resolveContactLocator(db, target.contact, "target.contact");
2886
2918
  if (!contact.ok) {
@@ -2962,7 +2994,8 @@ var manageAttributeTool = defineAffinityTool({
2962
2994
  target: objectSchema(
2963
2995
  {
2964
2996
  kind: enumSchema("Target kind.", ["contact", "link", "event"]),
2965
- eventId: integerSchema("Target event id."),
2997
+ eventId: integerSchema("Target event id; provide eventId or ref."),
2998
+ ref: externalRefSchema,
2966
2999
  contact: contactLocatorSchema2(
2967
3000
  "Contact locator. Required when kind=contact. Provide contactId or identity."
2968
3001
  ),
@@ -3018,7 +3051,9 @@ function insertJournalEvent(db, params) {
3018
3051
  );
3019
3052
  const eventId = Number(result.lastInsertRowid);
3020
3053
  db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
3021
- params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 ? 0 : 1 : Number(params.affectsBond),
3054
+ params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 || ["activity", "milestone", "observation"].includes(params.type) && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
3055
+ summary
3056
+ ) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
3022
3057
  eventId
3023
3058
  );
3024
3059
  db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
@@ -4690,7 +4725,7 @@ function exportContact(db, id) {
4690
4725
  const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
4691
4726
  if (!contact) throw new AffinityNotFoundError("contact not found");
4692
4727
  const events = query(
4693
- "SELECT DISTINCT e.*, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
4728
+ "SELECT DISTINCT e.*, COALESCE(e.parent_event_id, (SELECT parent.id FROM events parent WHERE parent.external_source=e.parent_source AND parent.external_id=e.parent_ref_id AND parent.deleted_at IS NULL)) AS parent_event_id, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
4694
4729
  id
4695
4730
  ).map((event) => ({
4696
4731
  ...event,
@@ -7831,12 +7866,14 @@ function getEventThread(db, source, id, options) {
7831
7866
  const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
7832
7867
  SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
7833
7868
  WHERE e.deleted_at IS NULL AND (e.parent_event_id=p.id OR (e.parent_source=p.external_source AND e.parent_ref_id=p.external_id) OR (e.thread_source=? AND e.thread_id=?))
7834
- ) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
7869
+ ) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) AND (? IS NULL OR e.type IN (SELECT value FROM json_each(?))) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
7835
7870
  root.id,
7836
7871
  root.externalRef?.source ?? null,
7837
7872
  root.externalRef?.id ?? null,
7838
7873
  options?.channel ?? null,
7839
7874
  options?.channel?.trim().toLowerCase() ?? null,
7875
+ options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
7876
+ options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
7840
7877
  limit,
7841
7878
  offset
7842
7879
  );
@@ -8488,12 +8525,12 @@ function reviewPage(db, rawInput, pageOnly) {
8488
8525
  if (!pageOnly && input.view !== "graph.chart") {
8489
8526
  const limit = input.limit ?? 50;
8490
8527
  const offset = input.offset ?? 0;
8491
- if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
8528
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
8492
8529
  return toolFailure(
8493
8530
  "protocol",
8494
8531
  "invalid_input",
8495
8532
  "Invalid pagination.",
8496
- "limit must be 1..250; offset must be non-negative"
8533
+ "limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
8497
8534
  );
8498
8535
  const all = [];
8499
8536
  let page;
@@ -8545,7 +8582,7 @@ function reviewPage(db, rawInput, pageOnly) {
8545
8582
  {
8546
8583
  ...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
8547
8584
  ...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
8548
- ...input.minScore === void 0 ? {} : { minScore: input.minScore },
8585
+ ...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
8549
8586
  ...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
8550
8587
  },
8551
8588
  options
@@ -8559,9 +8596,10 @@ function reviewPage(db, rawInput, pageOnly) {
8559
8596
  "Missing thread reference.",
8560
8597
  "ref is required for events.thread."
8561
8598
  );
8562
- items = getEventThread(db, input.ref.source, input.ref.id, options).map(
8563
- toEventListToolItem
8564
- );
8599
+ items = getEventThread(db, input.ref.source, input.ref.id, {
8600
+ ...options,
8601
+ ...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
8602
+ }).map(toEventListToolItem);
8565
8603
  break;
8566
8604
  }
8567
8605
  case "events.contact_journal": {
@@ -8820,7 +8858,7 @@ var reviewAffinityTool = defineAffinityTool({
8820
8858
  includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
8821
8859
  since: "Optional lower timestamp bound for time-based views.",
8822
8860
  until: "Optional upper timestamp bound for time-based views.",
8823
- limit: "Optional result limit.",
8861
+ limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
8824
8862
  offset: "Optional result offset."
8825
8863
  },
8826
8864
  outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
@@ -8925,13 +8963,17 @@ var reviewAffinityTool = defineAffinityTool({
8925
8963
  ]),
8926
8964
  horizonDays: integerSchema("Optional horizon-days filter."),
8927
8965
  exactOnly: booleanSchema("Whether duplicate review is exact-only."),
8928
- minScore: numberSchema("Optional duplicate minimum score."),
8966
+ minScore: numberSchema(
8967
+ "Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
8968
+ ),
8929
8969
  includeDismissed: booleanSchema(
8930
8970
  "Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
8931
8971
  ),
8932
8972
  since: integerSchema("Optional lower timestamp bound."),
8933
8973
  until: integerSchema("Optional upper timestamp bound."),
8934
- limit: integerSchema("Optional result limit."),
8974
+ limit: integerSchema(
8975
+ "Optional result limit, 1..10000 (default 50); use offset for paging."
8976
+ ),
8935
8977
  offset: integerSchema("Optional result offset.")
8936
8978
  },
8937
8979
  ["view"],
@@ -9225,10 +9267,18 @@ function normalizeTimeInputs(value, now = Date.now(), inPatch = false) {
9225
9267
  `Invalid timestamp "${item}". ${error.message}`
9226
9268
  );
9227
9269
  }
9270
+ if (!inPatch && ["occurredAt", "dueAt"].includes(key) && typeof item === "string" && /\.\d{4,}/.test(item)) {
9271
+ const provenance = result.provenance ?? value.provenance;
9272
+ result.provenance = {
9273
+ ...provenance && typeof provenance === "object" ? provenance : {},
9274
+ [`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: item
9275
+ };
9276
+ }
9228
9277
  result[key] = parsed.timestamp;
9229
9278
  if (!inPatch && ["occurredAt", "dueAt"].includes(key))
9230
9279
  result[key.replace(/At$/, "On")] = parsed.day;
9231
- } else result[key] = normalizeTimeInputs(item, now, key === "patch");
9280
+ } else if (key !== "provenance" || result.provenance === void 0)
9281
+ result[key] = key === "provenance" ? item : normalizeTimeInputs(item, now, key === "patch");
9232
9282
  }
9233
9283
  return result;
9234
9284
  }
@@ -9260,6 +9310,12 @@ function timestampSchema(schema) {
9260
9310
  function surfaceProperties(tool) {
9261
9311
  return {
9262
9312
  ...tool.inputSchema.properties,
9313
+ ...tool.name === "review_affinity" ? {
9314
+ includeContent: {
9315
+ type: "boolean",
9316
+ description: "Deprecated alias for --with-content; includes verbatim event content."
9317
+ }
9318
+ } : {},
9263
9319
  ...["inspect_affinity_item", "review_affinity", "manage_event"].includes(
9264
9320
  tool.name
9265
9321
  ) ? {
@@ -9860,9 +9916,9 @@ async function openAffinityDb(path) {
9860
9916
  }
9861
9917
  const db = new sqlite.DatabaseSync(path);
9862
9918
  try {
9863
- db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
9919
+ db.exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;");
9864
9920
  const context = db;
9865
- initAffinityTables(context);
9921
+ withTransaction(context, () => initAffinityTables(context));
9866
9922
  return context;
9867
9923
  } catch (error) {
9868
9924
  db.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostpaw/affinity",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Relationship intelligence and social CRM model for Node.js — SQLite, zero dependencies, with a built-in LLM tool facade",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -12,7 +12,7 @@ Use ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or
12
12
  Use externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.
13
13
  Participant contact locators may use identities, including did, instead of ids.
14
14
  Read JSON flags from stdin with --input - or from @file for large payloads (E2BIG).
15
- SQLite date-times without offsets mean UTC; microseconds round to milliseconds.
15
+ SQLite date-times without offsets mean UTC; microseconds truncate to milliseconds; raw timestamps remain in provenance.
16
16
  Missing inbound-follow timestamps should use a known first_seen or be counted as skipped.
17
17
  The synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs
18
18
  show the mapping and verify an idempotent retry and byte-identical text.
@@ -11,7 +11,7 @@ arrive. Commitments and resolutions never reset contact recency or cadence.
11
11
  Imported text and event maintenance:
12
12
  - Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.
13
13
  - Store verbatim text in content (up to 1,048,576 UTF-8 bytes, empty allowed). Summary is optional when content exists; its derived label is at most 140 characters.
14
- - Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId}.
14
+ - Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId} or {kind: "event", ref: {source, id}}. Unchanged values return no_op.
15
15
  - Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.
16
16
  - Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.
17
17
  - Use manage_event revise with patch: summary, content, occurredAt, significance, channel, type within the same kind, or externalRef only when absent. Unknown keys fail validation.
@@ -1,7 +1,8 @@
1
1
  # Record Observations And Referrals
2
2
 
3
3
  Likes, reposts and follows are ambient observations, not milestones. Record them
4
- with affectsBond:false; observations with significance <=2 default to false.
4
+ with affectsBond:false. Like/follow/repost labels default to false across families;
5
+ explicit true opts in. Observations with significance <=2 also default to false.
5
6
  They stay in journals but do not change affinity or meaningful-contact recency.
6
7
  provenance.ambient:true also suppresses effects. Use explicit owner and other
7
8
  participants, actor/recipient roles, and the correct directionality.
@@ -58,7 +58,7 @@ Read-only.
58
58
  | Parameter | Type | Required | Values / description |
59
59
  | --- | --- | --- | --- |
60
60
  | ref | object | no | |
61
- | includeContent | boolean | no | Include verbatim content in event rows; defaults to false. |
61
+ | includeContent | boolean | no | Deprecated alias for --with-content; includes verbatim event content. |
62
62
  | channel | string | no | Filter events by channel. |
63
63
  | view | string | yes | contacts.list, contacts.duplicates, events.thread, events.contact_journal, events.link_timeline, events.moments, commitments.open, links.owner, links.observed, links.progression_readiness, links.radar, dates.upcoming, graph.chart, merges.history, merges.dismissed. Which review surface to return. |
64
64
  | now | integer / string | no | Optional current timestamp for overdue calculations. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |
@@ -81,11 +81,11 @@ Read-only.
81
81
  | recurrenceKind | string | no | birthday, anniversary, renewal, memorial, custom_yearly. Optional recurrence-kind filter. |
82
82
  | horizonDays | integer | no | Optional horizon-days filter. |
83
83
  | exactOnly | boolean | no | Whether duplicate review is exact-only. |
84
- | minScore | number | no | Optional duplicate minimum score. |
84
+ | minScore | number | no | Optional duplicate minimum score (default 0.5; use 0 for all candidates). |
85
85
  | includeDismissed | boolean | no | Whether dismissed pairs are included in duplicate review (marked with dismissed flag). |
86
86
  | since | integer / string | no | Optional lower timestamp bound. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |
87
87
  | until | integer / string | no | Optional upper timestamp bound. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |
88
- | limit | integer | no | Optional result limit. |
88
+ | limit | integer | no | Optional result limit, 1..10000 (default 50); use offset for paging. |
89
89
  | offset | integer | no | Optional result offset. |
90
90
  | refSource | string | no | External reference source. |
91
91
  | refId | string | no | External reference id. |
@@ -302,7 +302,7 @@ Writes: writes_state.
302
302
  | kind | string | yes | interaction, observation, milestone, transaction. Event kind. |
303
303
  | input | object | yes | Event payload. Always required. Interaction events also require `type`. |
304
304
  | input.type | string | no | Interaction subtype. Required when kind=interaction, omit otherwise. |
305
- | input.affectsBond | boolean | no | Whether evidence affects the relationship; defaults false for observations with significance <=2. |
305
+ | input.affectsBond | boolean | no | Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in. |
306
306
  | input.content | string | no | Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied. |
307
307
  | input.externalRef | object | no | |
308
308
  | input.channel | string | no | Free-text channel, trimmed and lowercased, at most 40 characters. |
@@ -340,7 +340,7 @@ Writes: writes_state.
340
340
  | resolution | string | no | kept, cancelled, broken. How the commitment was resolved. Required when action=resolve. |
341
341
  | options | object | no | Optional metadata. |
342
342
  | input.commitmentType | string | yes | Promise or agreement. |
343
- | input.affectsBond | boolean | no | Whether evidence affects the relationship; defaults false for observations with significance <=2. |
343
+ | input.affectsBond | boolean | no | Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in. |
344
344
  | input.content | string | no | Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied. |
345
345
  | input.externalRef | object | no | |
346
346
  | input.channel | string | no | Free-text channel, trimmed and lowercased, at most 40 characters. |
@@ -421,7 +421,8 @@ Writes: writes_state.
421
421
  | entries | array | no | Key-value map to replace all attributes. Required when action=replace. |
422
422
  | removedAt | integer / string | no | Removal timestamp. Optional for action=unset. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |
423
423
  | target.kind | string | yes | Target kind. |
424
- | target.eventId | integer | no | Target event id. |
424
+ | target.eventId | integer | no | Target event id; provide eventId or ref. |
425
+ | target.ref | object | no | |
425
426
  | target.contact | object | no | Contact locator. Required when kind=contact. Provide contactId or identity. |
426
427
  | target.link | object | no | Link locator. Required when kind=link. Provide linkId or endpoints. |
427
428
  | entries.name | string | yes | Attribute name. |