@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/index.cjs CHANGED
@@ -1399,6 +1399,8 @@ function mapEventRowToEventRecord(row, participants) {
1399
1399
  momentKind: row.moment_kind,
1400
1400
  participants
1401
1401
  };
1402
+ if (row.provenance_json != null)
1403
+ base.provenance = JSON.parse(row.provenance_json);
1402
1404
  if (row.content != null) base.content = decodeEventText(row.content);
1403
1405
  if (row.channel != null) base.channel = row.channel;
1404
1406
  if (row.external_source != null && row.external_id != null)
@@ -1689,14 +1691,9 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
1689
1691
  text = text.replace(" ", "T");
1690
1692
  if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
1691
1693
  }
1692
- let rounding = 0;
1693
1694
  text = text.replace(
1694
1695
  /\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
1695
- (_, digits) => {
1696
- const ms = Math.round(Number(`0.${digits}`) * 1e3);
1697
- rounding = ms === 1e3 ? 1 : 0;
1698
- return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
1699
- }
1696
+ (_, digits) => `.${digits.slice(0, 3)}`
1700
1697
  );
1701
1698
  if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
1702
1699
  const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
@@ -1716,7 +1713,7 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
1716
1713
  const midnight = Date.parse(`${day}T00:00:00Z`);
1717
1714
  if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
1718
1715
  return fail();
1719
- const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text) + rounding;
1716
+ const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
1720
1717
  if (!Number.isFinite(timestamp)) return fail();
1721
1718
  return { timestamp, day };
1722
1719
  }
@@ -2437,7 +2434,9 @@ function insertJournalEvent(db, params) {
2437
2434
  );
2438
2435
  const eventId = Number(result.lastInsertRowid);
2439
2436
  db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
2440
- params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 ? 0 : 1 : Number(params.affectsBond),
2437
+ 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(
2438
+ summary
2439
+ ) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
2441
2440
  eventId
2442
2441
  );
2443
2442
  db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
@@ -5634,7 +5633,7 @@ function exportContact(db, id) {
5634
5633
  const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
5635
5634
  if (!contact) throw new AffinityNotFoundError("contact not found");
5636
5635
  const events = query(
5637
- "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",
5636
+ "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",
5638
5637
  id
5639
5638
  ).map((event) => ({
5640
5639
  ...event,
@@ -5762,12 +5761,14 @@ function getEventThread(db, source, id, options) {
5762
5761
  const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
5763
5762
  SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
5764
5763
  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=?))
5765
- ) 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(
5764
+ ) 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(
5766
5765
  root.id,
5767
5766
  root.externalRef?.source ?? null,
5768
5767
  root.externalRef?.id ?? null,
5769
5768
  options?.channel ?? null,
5770
5769
  options?.channel?.trim().toLowerCase() ?? null,
5770
+ options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
5771
+ options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
5771
5772
  limit,
5772
5773
  offset
5773
5774
  );
@@ -6062,7 +6063,7 @@ Use ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or
6062
6063
  Use externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.
6063
6064
  Participant contact locators may use identities, including did, instead of ids.
6064
6065
  Read JSON flags from stdin with --input - or from @file for large payloads (E2BIG).
6065
- SQLite date-times without offsets mean UTC; microseconds round to milliseconds.
6066
+ SQLite date-times without offsets mean UTC; microseconds truncate to milliseconds; raw timestamps remain in provenance.
6066
6067
  Missing inbound-follow timestamps should use a known first_seen or be counted as skipped.
6067
6068
  The synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs
6068
6069
  show the mapping and verify an idempotent retry and byte-identical text.
@@ -6324,7 +6325,7 @@ arrive. Commitments and resolutions never reset contact recency or cadence.
6324
6325
  Imported text and event maintenance:
6325
6326
  - Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.
6326
6327
  - 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.
6327
- - Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId}.
6328
+ - 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.
6328
6329
  - Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.
6329
6330
  - Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.
6330
6331
  - 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.
@@ -6390,7 +6391,8 @@ var recordObservationsAndReferralsSkill = defineAffinitySkill({
6390
6391
  content: `# Record Observations And Referrals
6391
6392
 
6392
6393
  Likes, reposts and follows are ambient observations, not milestones. Record them
6393
- with affectsBond:false; observations with significance <=2 default to false.
6394
+ with affectsBond:false. Like/follow/repost labels default to false across families;
6395
+ explicit true opts in. Observations with significance <=2 also default to false.
6394
6396
  They stay in journals but do not change affinity or meaningful-contact recency.
6395
6397
  provenance.ambient:true also suppresses effects. Use explicit owner and other
6396
6398
  participants, actor/recipient roles, and the correct directionality.
@@ -6769,7 +6771,7 @@ var externalRefSchema = objectSchema(
6769
6771
  );
6770
6772
  var eventMetadataSchema = {
6771
6773
  affectsBond: booleanSchema(
6772
- "Whether evidence affects the relationship; defaults false for observations with significance <=2."
6774
+ "Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
6773
6775
  ),
6774
6776
  content: stringSchema(
6775
6777
  "Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
@@ -7308,6 +7310,13 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
7308
7310
  `${path}.${key}`,
7309
7311
  referenceNow
7310
7312
  );
7313
+ if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
7314
+ const provenance = object.provenance;
7315
+ object.provenance = {
7316
+ ...provenance && typeof provenance === "object" ? provenance : {},
7317
+ [`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
7318
+ };
7319
+ }
7311
7320
  object[key] = parsed.timestamp;
7312
7321
  if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
7313
7322
  object[key.replace(/At$/, "On")] ??= parsed.day;
@@ -7325,7 +7334,7 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
7325
7334
  const error = validateToolInput(
7326
7335
  child,
7327
7336
  object[key],
7328
- `${path}.${key}`,
7337
+ key === "input" && path === "input" ? path : `${path}.${key}`,
7329
7338
  referenceNow,
7330
7339
  strict
7331
7340
  );
@@ -7719,6 +7728,11 @@ function setAttribute(db, target, name, value) {
7719
7728
  const stored = normalizeAttributeValue(value);
7720
7729
  const now = resolveNow();
7721
7730
  const existing = getLiveAttributeRow(db, target, key);
7731
+ if (existing?.value === stored)
7732
+ return buildAttributeMutationReceipt(
7733
+ mapAttributeRowToAttributeRecord(existing),
7734
+ {}
7735
+ );
7722
7736
  if (existing) {
7723
7737
  db.prepare(
7724
7738
  "UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
@@ -7858,7 +7872,30 @@ function linkLocatorSchema2(description) {
7858
7872
  };
7859
7873
  }
7860
7874
  function resolveAttributeTarget(db, target) {
7861
- if (target.kind === "event") return { kind: "ok", target };
7875
+ if (target.kind === "event") {
7876
+ if (target.eventId === void 0 === (target.ref === void 0))
7877
+ return {
7878
+ kind: "error",
7879
+ result: toolFailure(
7880
+ "protocol",
7881
+ "invalid_input",
7882
+ "Invalid event target.",
7883
+ "Provide exactly one of target.eventId or target.ref."
7884
+ )
7885
+ };
7886
+ const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
7887
+ if (eventId === void 0)
7888
+ return {
7889
+ kind: "error",
7890
+ result: toolFailure(
7891
+ "domain",
7892
+ "not_found",
7893
+ "Event not found.",
7894
+ "No event matches target.ref."
7895
+ )
7896
+ };
7897
+ return { kind: "ok", target: { kind: "event", eventId } };
7898
+ }
7862
7899
  if (target.kind === "contact") {
7863
7900
  const contact = resolveContactLocator(db, target.contact, "target.contact");
7864
7901
  if (!contact.ok) {
@@ -7940,7 +7977,8 @@ var manageAttributeTool = defineAffinityTool({
7940
7977
  target: objectSchema(
7941
7978
  {
7942
7979
  kind: enumSchema("Target kind.", ["contact", "link", "event"]),
7943
- eventId: integerSchema("Target event id."),
7980
+ eventId: integerSchema("Target event id; provide eventId or ref."),
7981
+ ref: externalRefSchema,
7944
7982
  contact: contactLocatorSchema2(
7945
7983
  "Contact locator. Required when kind=contact. Provide contactId or identity."
7946
7984
  ),
@@ -9816,12 +9854,12 @@ function reviewPage(db, rawInput, pageOnly) {
9816
9854
  if (!pageOnly && input.view !== "graph.chart") {
9817
9855
  const limit = input.limit ?? 50;
9818
9856
  const offset = input.offset ?? 0;
9819
- if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
9857
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
9820
9858
  return toolFailure(
9821
9859
  "protocol",
9822
9860
  "invalid_input",
9823
9861
  "Invalid pagination.",
9824
- "limit must be 1..250; offset must be non-negative"
9862
+ "limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
9825
9863
  );
9826
9864
  const all = [];
9827
9865
  let page;
@@ -9873,7 +9911,7 @@ function reviewPage(db, rawInput, pageOnly) {
9873
9911
  {
9874
9912
  ...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
9875
9913
  ...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
9876
- ...input.minScore === void 0 ? {} : { minScore: input.minScore },
9914
+ ...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
9877
9915
  ...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
9878
9916
  },
9879
9917
  options
@@ -9887,9 +9925,10 @@ function reviewPage(db, rawInput, pageOnly) {
9887
9925
  "Missing thread reference.",
9888
9926
  "ref is required for events.thread."
9889
9927
  );
9890
- items = getEventThread(db, input.ref.source, input.ref.id, options).map(
9891
- toEventListToolItem
9892
- );
9928
+ items = getEventThread(db, input.ref.source, input.ref.id, {
9929
+ ...options,
9930
+ ...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
9931
+ }).map(toEventListToolItem);
9893
9932
  break;
9894
9933
  }
9895
9934
  case "events.contact_journal": {
@@ -10148,7 +10187,7 @@ var reviewAffinityTool = defineAffinityTool({
10148
10187
  includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
10149
10188
  since: "Optional lower timestamp bound for time-based views.",
10150
10189
  until: "Optional upper timestamp bound for time-based views.",
10151
- limit: "Optional result limit.",
10190
+ limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
10152
10191
  offset: "Optional result offset."
10153
10192
  },
10154
10193
  outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
@@ -10253,13 +10292,17 @@ var reviewAffinityTool = defineAffinityTool({
10253
10292
  ]),
10254
10293
  horizonDays: integerSchema("Optional horizon-days filter."),
10255
10294
  exactOnly: booleanSchema("Whether duplicate review is exact-only."),
10256
- minScore: numberSchema("Optional duplicate minimum score."),
10295
+ minScore: numberSchema(
10296
+ "Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
10297
+ ),
10257
10298
  includeDismissed: booleanSchema(
10258
10299
  "Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
10259
10300
  ),
10260
10301
  since: integerSchema("Optional lower timestamp bound."),
10261
10302
  until: integerSchema("Optional upper timestamp bound."),
10262
- limit: integerSchema("Optional result limit."),
10303
+ limit: integerSchema(
10304
+ "Optional result limit, 1..10000 (default 50); use offset for paging."
10305
+ ),
10263
10306
  offset: integerSchema("Optional result offset.")
10264
10307
  },
10265
10308
  ["view"],
package/dist/index.d.cts CHANGED
@@ -273,6 +273,7 @@ interface ExternalRef {
273
273
  /** Journal entry with explicit participants — CONCEPT.md EventRecord. */
274
274
  interface EventRecord {
275
275
  content?: string;
276
+ provenance?: unknown;
276
277
  externalRef?: ExternalRef;
277
278
  channel?: string;
278
279
  parentRef?: ExternalRef;
@@ -846,6 +847,7 @@ declare function searchEvents(db: AffinityDb, query: string, options?: {
846
847
  offset?: number;
847
848
  }): EventSearchItem[];
848
849
  declare function getEventThread(db: AffinityDb, source: string, id: string, options?: {
850
+ eventTypes?: readonly EventType[];
849
851
  includeContent?: boolean;
850
852
  channel?: string;
851
853
  limit?: number;
@@ -1491,6 +1493,11 @@ type AttributeTargetInput = {
1491
1493
  } | {
1492
1494
  kind: "event";
1493
1495
  eventId: number;
1496
+ ref?: never;
1497
+ } | {
1498
+ kind: "event";
1499
+ ref: ExternalRef;
1500
+ eventId?: never;
1494
1501
  };
1495
1502
  type ManageAttributeToolInput = {
1496
1503
  action: "set";
package/dist/index.d.ts CHANGED
@@ -273,6 +273,7 @@ interface ExternalRef {
273
273
  /** Journal entry with explicit participants — CONCEPT.md EventRecord. */
274
274
  interface EventRecord {
275
275
  content?: string;
276
+ provenance?: unknown;
276
277
  externalRef?: ExternalRef;
277
278
  channel?: string;
278
279
  parentRef?: ExternalRef;
@@ -846,6 +847,7 @@ declare function searchEvents(db: AffinityDb, query: string, options?: {
846
847
  offset?: number;
847
848
  }): EventSearchItem[];
848
849
  declare function getEventThread(db: AffinityDb, source: string, id: string, options?: {
850
+ eventTypes?: readonly EventType[];
849
851
  includeContent?: boolean;
850
852
  channel?: string;
851
853
  limit?: number;
@@ -1491,6 +1493,11 @@ type AttributeTargetInput = {
1491
1493
  } | {
1492
1494
  kind: "event";
1493
1495
  eventId: number;
1496
+ ref?: never;
1497
+ } | {
1498
+ kind: "event";
1499
+ ref: ExternalRef;
1500
+ eventId?: never;
1494
1501
  };
1495
1502
  type ManageAttributeToolInput = {
1496
1503
  action: "set";
package/dist/index.js CHANGED
@@ -1397,6 +1397,8 @@ function mapEventRowToEventRecord(row, participants) {
1397
1397
  momentKind: row.moment_kind,
1398
1398
  participants
1399
1399
  };
1400
+ if (row.provenance_json != null)
1401
+ base.provenance = JSON.parse(row.provenance_json);
1400
1402
  if (row.content != null) base.content = decodeEventText(row.content);
1401
1403
  if (row.channel != null) base.channel = row.channel;
1402
1404
  if (row.external_source != null && row.external_id != null)
@@ -1687,14 +1689,9 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
1687
1689
  text = text.replace(" ", "T");
1688
1690
  if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
1689
1691
  }
1690
- let rounding = 0;
1691
1692
  text = text.replace(
1692
1693
  /\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
1693
- (_, digits) => {
1694
- const ms = Math.round(Number(`0.${digits}`) * 1e3);
1695
- rounding = ms === 1e3 ? 1 : 0;
1696
- return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
1697
- }
1694
+ (_, digits) => `.${digits.slice(0, 3)}`
1698
1695
  );
1699
1696
  if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
1700
1697
  const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
@@ -1714,7 +1711,7 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
1714
1711
  const midnight = Date.parse(`${day}T00:00:00Z`);
1715
1712
  if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
1716
1713
  return fail();
1717
- const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text) + rounding;
1714
+ const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
1718
1715
  if (!Number.isFinite(timestamp)) return fail();
1719
1716
  return { timestamp, day };
1720
1717
  }
@@ -2435,7 +2432,9 @@ function insertJournalEvent(db, params) {
2435
2432
  );
2436
2433
  const eventId = Number(result.lastInsertRowid);
2437
2434
  db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
2438
- params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 ? 0 : 1 : Number(params.affectsBond),
2435
+ 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(
2436
+ summary
2437
+ ) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
2439
2438
  eventId
2440
2439
  );
2441
2440
  db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
@@ -5632,7 +5631,7 @@ function exportContact(db, id) {
5632
5631
  const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
5633
5632
  if (!contact) throw new AffinityNotFoundError("contact not found");
5634
5633
  const events = query(
5635
- "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",
5634
+ "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",
5636
5635
  id
5637
5636
  ).map((event) => ({
5638
5637
  ...event,
@@ -5760,12 +5759,14 @@ function getEventThread(db, source, id, options) {
5760
5759
  const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
5761
5760
  SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
5762
5761
  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=?))
5763
- ) 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(
5762
+ ) 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(
5764
5763
  root.id,
5765
5764
  root.externalRef?.source ?? null,
5766
5765
  root.externalRef?.id ?? null,
5767
5766
  options?.channel ?? null,
5768
5767
  options?.channel?.trim().toLowerCase() ?? null,
5768
+ options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
5769
+ options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
5769
5770
  limit,
5770
5771
  offset
5771
5772
  );
@@ -6060,7 +6061,7 @@ Use ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or
6060
6061
  Use externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.
6061
6062
  Participant contact locators may use identities, including did, instead of ids.
6062
6063
  Read JSON flags from stdin with --input - or from @file for large payloads (E2BIG).
6063
- SQLite date-times without offsets mean UTC; microseconds round to milliseconds.
6064
+ SQLite date-times without offsets mean UTC; microseconds truncate to milliseconds; raw timestamps remain in provenance.
6064
6065
  Missing inbound-follow timestamps should use a known first_seen or be counted as skipped.
6065
6066
  The synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs
6066
6067
  show the mapping and verify an idempotent retry and byte-identical text.
@@ -6322,7 +6323,7 @@ arrive. Commitments and resolutions never reset contact recency or cadence.
6322
6323
  Imported text and event maintenance:
6323
6324
  - Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.
6324
6325
  - 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.
6325
- - Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId}.
6326
+ - 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.
6326
6327
  - Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.
6327
6328
  - Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.
6328
6329
  - 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.
@@ -6388,7 +6389,8 @@ var recordObservationsAndReferralsSkill = defineAffinitySkill({
6388
6389
  content: `# Record Observations And Referrals
6389
6390
 
6390
6391
  Likes, reposts and follows are ambient observations, not milestones. Record them
6391
- with affectsBond:false; observations with significance <=2 default to false.
6392
+ with affectsBond:false. Like/follow/repost labels default to false across families;
6393
+ explicit true opts in. Observations with significance <=2 also default to false.
6392
6394
  They stay in journals but do not change affinity or meaningful-contact recency.
6393
6395
  provenance.ambient:true also suppresses effects. Use explicit owner and other
6394
6396
  participants, actor/recipient roles, and the correct directionality.
@@ -6767,7 +6769,7 @@ var externalRefSchema = objectSchema(
6767
6769
  );
6768
6770
  var eventMetadataSchema = {
6769
6771
  affectsBond: booleanSchema(
6770
- "Whether evidence affects the relationship; defaults false for observations with significance <=2."
6772
+ "Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
6771
6773
  ),
6772
6774
  content: stringSchema(
6773
6775
  "Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
@@ -7306,6 +7308,13 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
7306
7308
  `${path}.${key}`,
7307
7309
  referenceNow
7308
7310
  );
7311
+ if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
7312
+ const provenance = object.provenance;
7313
+ object.provenance = {
7314
+ ...provenance && typeof provenance === "object" ? provenance : {},
7315
+ [`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
7316
+ };
7317
+ }
7309
7318
  object[key] = parsed.timestamp;
7310
7319
  if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
7311
7320
  object[key.replace(/At$/, "On")] ??= parsed.day;
@@ -7323,7 +7332,7 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
7323
7332
  const error = validateToolInput(
7324
7333
  child,
7325
7334
  object[key],
7326
- `${path}.${key}`,
7335
+ key === "input" && path === "input" ? path : `${path}.${key}`,
7327
7336
  referenceNow,
7328
7337
  strict
7329
7338
  );
@@ -7717,6 +7726,11 @@ function setAttribute(db, target, name, value) {
7717
7726
  const stored = normalizeAttributeValue(value);
7718
7727
  const now = resolveNow();
7719
7728
  const existing = getLiveAttributeRow(db, target, key);
7729
+ if (existing?.value === stored)
7730
+ return buildAttributeMutationReceipt(
7731
+ mapAttributeRowToAttributeRecord(existing),
7732
+ {}
7733
+ );
7720
7734
  if (existing) {
7721
7735
  db.prepare(
7722
7736
  "UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
@@ -7856,7 +7870,30 @@ function linkLocatorSchema2(description) {
7856
7870
  };
7857
7871
  }
7858
7872
  function resolveAttributeTarget(db, target) {
7859
- if (target.kind === "event") return { kind: "ok", target };
7873
+ if (target.kind === "event") {
7874
+ if (target.eventId === void 0 === (target.ref === void 0))
7875
+ return {
7876
+ kind: "error",
7877
+ result: toolFailure(
7878
+ "protocol",
7879
+ "invalid_input",
7880
+ "Invalid event target.",
7881
+ "Provide exactly one of target.eventId or target.ref."
7882
+ )
7883
+ };
7884
+ const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
7885
+ if (eventId === void 0)
7886
+ return {
7887
+ kind: "error",
7888
+ result: toolFailure(
7889
+ "domain",
7890
+ "not_found",
7891
+ "Event not found.",
7892
+ "No event matches target.ref."
7893
+ )
7894
+ };
7895
+ return { kind: "ok", target: { kind: "event", eventId } };
7896
+ }
7860
7897
  if (target.kind === "contact") {
7861
7898
  const contact = resolveContactLocator(db, target.contact, "target.contact");
7862
7899
  if (!contact.ok) {
@@ -7938,7 +7975,8 @@ var manageAttributeTool = defineAffinityTool({
7938
7975
  target: objectSchema(
7939
7976
  {
7940
7977
  kind: enumSchema("Target kind.", ["contact", "link", "event"]),
7941
- eventId: integerSchema("Target event id."),
7978
+ eventId: integerSchema("Target event id; provide eventId or ref."),
7979
+ ref: externalRefSchema,
7942
7980
  contact: contactLocatorSchema2(
7943
7981
  "Contact locator. Required when kind=contact. Provide contactId or identity."
7944
7982
  ),
@@ -9814,12 +9852,12 @@ function reviewPage(db, rawInput, pageOnly) {
9814
9852
  if (!pageOnly && input.view !== "graph.chart") {
9815
9853
  const limit = input.limit ?? 50;
9816
9854
  const offset = input.offset ?? 0;
9817
- if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
9855
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
9818
9856
  return toolFailure(
9819
9857
  "protocol",
9820
9858
  "invalid_input",
9821
9859
  "Invalid pagination.",
9822
- "limit must be 1..250; offset must be non-negative"
9860
+ "limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
9823
9861
  );
9824
9862
  const all = [];
9825
9863
  let page;
@@ -9871,7 +9909,7 @@ function reviewPage(db, rawInput, pageOnly) {
9871
9909
  {
9872
9910
  ...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
9873
9911
  ...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
9874
- ...input.minScore === void 0 ? {} : { minScore: input.minScore },
9912
+ ...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
9875
9913
  ...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
9876
9914
  },
9877
9915
  options
@@ -9885,9 +9923,10 @@ function reviewPage(db, rawInput, pageOnly) {
9885
9923
  "Missing thread reference.",
9886
9924
  "ref is required for events.thread."
9887
9925
  );
9888
- items = getEventThread(db, input.ref.source, input.ref.id, options).map(
9889
- toEventListToolItem
9890
- );
9926
+ items = getEventThread(db, input.ref.source, input.ref.id, {
9927
+ ...options,
9928
+ ...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
9929
+ }).map(toEventListToolItem);
9891
9930
  break;
9892
9931
  }
9893
9932
  case "events.contact_journal": {
@@ -10146,7 +10185,7 @@ var reviewAffinityTool = defineAffinityTool({
10146
10185
  includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
10147
10186
  since: "Optional lower timestamp bound for time-based views.",
10148
10187
  until: "Optional upper timestamp bound for time-based views.",
10149
- limit: "Optional result limit.",
10188
+ limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
10150
10189
  offset: "Optional result offset."
10151
10190
  },
10152
10191
  outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
@@ -10251,13 +10290,17 @@ var reviewAffinityTool = defineAffinityTool({
10251
10290
  ]),
10252
10291
  horizonDays: integerSchema("Optional horizon-days filter."),
10253
10292
  exactOnly: booleanSchema("Whether duplicate review is exact-only."),
10254
- minScore: numberSchema("Optional duplicate minimum score."),
10293
+ minScore: numberSchema(
10294
+ "Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
10295
+ ),
10255
10296
  includeDismissed: booleanSchema(
10256
10297
  "Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
10257
10298
  ),
10258
10299
  since: integerSchema("Optional lower timestamp bound."),
10259
10300
  until: integerSchema("Optional upper timestamp bound."),
10260
- limit: integerSchema("Optional result limit."),
10301
+ limit: integerSchema(
10302
+ "Optional result limit, 1..10000 (default 50); use offset for paging."
10303
+ ),
10261
10304
  offset: integerSchema("Optional result offset.")
10262
10305
  },
10263
10306
  ["view"],