@parall/agent-core 1.42.1 → 1.43.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 (45) hide show
  1. package/dist/bin/channel-exec.d.ts +4 -0
  2. package/dist/bin/channel-exec.d.ts.map +1 -0
  3. package/dist/bin/channel-exec.js +246 -0
  4. package/dist/channel-capability.d.ts +16 -0
  5. package/dist/channel-capability.d.ts.map +1 -0
  6. package/dist/channel-capability.js +155 -0
  7. package/dist/channel-token.d.ts +19 -0
  8. package/dist/channel-token.d.ts.map +1 -0
  9. package/dist/channel-token.js +73 -0
  10. package/dist/event-format.d.ts.map +1 -1
  11. package/dist/event-format.js +21 -16
  12. package/dist/gateway-base.d.ts +12 -0
  13. package/dist/gateway-base.d.ts.map +1 -1
  14. package/dist/gateway-base.js +30 -3
  15. package/dist/gateway-lane-flow.d.ts +20 -1
  16. package/dist/gateway-lane-flow.d.ts.map +1 -1
  17. package/dist/gateway-lane-flow.js +75 -6
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -0
  21. package/dist/platform-config.d.ts +15 -0
  22. package/dist/platform-config.d.ts.map +1 -1
  23. package/dist/platform-config.js +28 -0
  24. package/dist/prompt-fragments.d.ts +1 -1
  25. package/dist/prompt-fragments.d.ts.map +1 -1
  26. package/dist/prompt-fragments.js +29 -7
  27. package/dist/skills/index.js +1 -1
  28. package/dist/skills/parall-platform.d.ts +1 -1
  29. package/dist/skills/parall-platform.d.ts.map +1 -1
  30. package/dist/skills/parall-platform.js +23 -4
  31. package/dist/types.d.ts +5 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/bin/channel-exec.ts +262 -0
  35. package/src/channel-capability.ts +187 -0
  36. package/src/channel-token.ts +92 -0
  37. package/src/event-format.ts +21 -16
  38. package/src/gateway-base.ts +44 -6
  39. package/src/gateway-lane-flow.ts +89 -4
  40. package/src/index.ts +3 -0
  41. package/src/platform-config.ts +44 -0
  42. package/src/prompt-fragments.ts +29 -7
  43. package/src/skills/index.ts +1 -1
  44. package/src/skills/parall-platform.ts +23 -4
  45. package/src/types.ts +5 -1
@@ -148,6 +148,14 @@ export type ParallGatewayOptions = {
148
148
  * this directory. Absent → legacy received/ack flow (openclaw / hermes).
149
149
  */
150
150
  dispatchContextDir?: string;
151
+ /**
152
+ * Live view of the agent's platform-granted capability keys (bridges wire
153
+ * PlatformConfigManager.capabilities().map(c => c.key)). Read at
154
+ * channel-event build time so the reply hint routes to the capability
155
+ * affordance (e.g. feishu-cli → lark-cli) — the single outbound path.
156
+ * Absent/empty → the hint states outbound is disabled.
157
+ */
158
+ getCapabilityKeys?: () => string[];
151
159
  onConfigUpdate?: (data: AgentConfigUpdateData) => Promise<void> | void;
152
160
  onSessionReady?: (state: {
153
161
  activeSessionId?: string;
@@ -293,6 +301,9 @@ export class ParallAgentGateway {
293
301
  // (stable mapping; avoids one connection fetch per inbound message).
294
302
  private readonly channelConnectionProviders = new Map<string, string>();
295
303
  private readonly dispatchedMessages = new Set<string>();
304
+ // Per-WorkItem failure backoff for typed dispatch consumption — see
305
+ // LaneFlowHost.typedRedriveBackoff in gateway-lane-flow.ts.
306
+ readonly typedRedriveBackoff = new Map<string, { failures: number; until: number }>();
296
307
  private readonly forkStates = new Map<string, ActiveForkState>();
297
308
  private readonly dispatchState: DispatchState = {
298
309
  mainDispatching: false,
@@ -448,8 +459,17 @@ export class ParallAgentGateway {
448
459
  source_id: data.id,
449
460
  })
450
461
  .then(
451
- () => undefined,
452
- () => undefined,
462
+ () => true,
463
+ (err) => {
464
+ // Same contract as ackDispatchEvent: a failed ack is a
465
+ // failed consume (arms backoff) and must free the hot-path
466
+ // dedupe so the re-drive isn't rejected by this pod forever.
467
+ this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
468
+ this.opts.log?.warn(
469
+ `dispatch ack failed for task ${data.id}, releasing for re-drive: ${String(err)}`,
470
+ );
471
+ return false;
472
+ },
453
473
  );
454
474
  },
455
475
  );
@@ -680,17 +700,20 @@ export class ParallAgentGateway {
680
700
  private consumeTypedDispatch(
681
701
  ref: { dispatchEventId?: string; sourceType?: string; sourceId?: string },
682
702
  run: (dispatchEventId?: string) => Promise<boolean>,
683
- ack: (dispatchEventId?: string) => void | Promise<void>,
703
+ ack: (dispatchEventId?: string) => boolean | void | Promise<boolean | void>,
684
704
  ): Promise<void> {
685
705
  return consumeTypedDispatch(this.laneFlowHost(), ref, run, ack);
686
706
  }
687
707
 
688
708
  // Typed completion must wait until the administrative ack has either
689
709
  // committed or failed. Errors stay best-effort: a failed ack leaves the row
690
- // received, so Complete releases and re-drives it safely.
691
- private ackDispatchEvent(dispatchEventId: string, onFailure?: () => void): Promise<void> {
710
+ // received, so Complete releases and re-drives it safely. The boolean
711
+ // outcome feeds the typed-consume backoff an ack that failed must count
712
+ // as a failed consume, or an ack outage would clear the backoff entry and
713
+ // let the release re-drive spin at wire speed.
714
+ private ackDispatchEvent(dispatchEventId: string, onFailure?: () => void): Promise<boolean> {
692
715
  return this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).then(
693
- () => undefined,
716
+ () => true,
694
717
  (err) => {
695
718
  // Complete will return the still-received item to pending and publish
696
719
  // an immediate hint. Free its hot-path claim first, otherwise that
@@ -699,6 +722,7 @@ export class ParallAgentGateway {
699
722
  this.opts.log?.warn(
700
723
  `dispatch ack failed for ${dispatchEventId}, releasing for re-drive: ${String(err)}`,
701
724
  );
725
+ return false;
702
726
  },
703
727
  );
704
728
  }
@@ -2471,6 +2495,19 @@ export class ParallAgentGateway {
2471
2495
  }
2472
2496
  }
2473
2497
 
2498
+ // The reply hint routes on the live capability grant: `<provider>-cli`
2499
+ // present → the vendor CLI is on PATH (broker shim) and is THE reply
2500
+ // path; absent → outbound is disabled for this org (flag/connection off)
2501
+ // and the hint must say so instead of pointing at a retired clip. The
2502
+ // provider label lookup above is best-effort/cosmetic — when it fails,
2503
+ // ANY granted `*-cli` capability keeps the hint on the CLI path: a
2504
+ // transient metadata miss must not flip an actively granted agent's
2505
+ // hint to "outbound disabled" and strand a valid external message.
2506
+ const keys = this.opts.getCapabilityKeys?.() ?? [];
2507
+ const cliCapable = provider
2508
+ ? keys.includes(`${provider}-cli`)
2509
+ : keys.some((k) => k.endsWith('-cli'));
2510
+
2474
2511
  const event: ParallEvent = {
2475
2512
  type: 'channel_message',
2476
2513
  targetId: conv.id,
@@ -2485,6 +2522,7 @@ export class ParallAgentGateway {
2485
2522
  channelConversationType: conv.conversation_type || undefined,
2486
2523
  channelExternalConversationId: conv.external_conversation_id,
2487
2524
  channelExternalMessageId: msg.external_message_id,
2525
+ channelCliCapable: cliCapable,
2488
2526
  ackSourceType: 'channel_message',
2489
2527
  ackSourceId: msg.id,
2490
2528
  };
@@ -18,6 +18,15 @@ export interface LaneFlowHost {
18
18
  ledgerDisabled: boolean;
19
19
  shuttingDown: boolean;
20
20
  dispatchedMessages: Set<string>;
21
+ /**
22
+ * Per-WorkItem failure backoff for typed dispatch consumption. A consume
23
+ * that ends without an ack re-arms the entry; the next attempt for the
24
+ * same WorkItem is delayed (not skipped — a skipped attempt would strand
25
+ * the pending row until reconnect catch-up) so a claim→fail→complete
26
+ * tight loop is throttled to exponential intervals instead of spinning at
27
+ * wire speed against the server's immediate re-drive (2026-07-11 OOM).
28
+ */
29
+ typedRedriveBackoff: Map<string, { failures: number; until: number }>;
21
30
  opts: {
22
31
  client: ParallClient;
23
32
  log?: GatewayLogger;
@@ -122,21 +131,91 @@ export async function dispatchLaneGroup(
122
131
  return 'dispatched';
123
132
  }
124
133
 
134
+ /** Failure backoff pacing for typed dispatch retries (base 2s, cap 5min). */
135
+ const TYPED_BACKOFF_BASE_MS = 2_000;
136
+ const TYPED_BACKOFF_CAP_MS = 5 * 60_000;
137
+ const TYPED_BACKOFF_MAP_CAP = 512;
138
+
125
139
  /**
126
140
  * Consume one typed dispatch (task/comment/schedule/trigger/approval) under
127
141
  * its typed-lane occupancy guard: claim the dsp:<id> lane (skip when another
128
142
  * pod holds it or the WorkItem is already resolved), run the handler, ack on
129
143
  * success (the doc's option (b): notification delivered, tracked elsewhere),
130
144
  * then release the lane. Legacy run+ack flow when the ledger is unavailable.
145
+ *
146
+ * Repeated failures back off: a consume that ends un-acked re-arms the
147
+ * WorkItem's backoff entry, and the next attempt sleeps out the remaining
148
+ * window before claiming. Without this, complete's release re-drives the
149
+ * item instantly and a persistently-failing consume (e.g. buffered behind a
150
+ * saturated fork pool) spins at wire speed — the client half of the
151
+ * 2026-07-11 poison loop (the server half is the redrive budget).
131
152
  */
132
153
  export async function consumeTypedDispatch(
133
154
  host: LaneFlowHost,
134
155
  ref: { dispatchEventId?: string; sourceType?: string; sourceId?: string },
135
156
  run: (dispatchEventId?: string) => Promise<boolean>,
136
- ack: (dispatchEventId?: string) => void | Promise<void>,
157
+ ack: (dispatchEventId?: string) => boolean | void | Promise<boolean | void>,
137
158
  ): Promise<void> {
159
+ const backoffKey = ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`;
160
+ const armed = host.typedRedriveBackoff.get(backoffKey);
161
+ if (armed) {
162
+ const waitMs = armed.until - Date.now();
163
+ if (waitMs > 0) {
164
+ host.opts.log?.info(
165
+ `typed dispatch ${backoffKey} backing off ${Math.ceil(waitMs / 1000)}s after ${armed.failures} failed consume(s)`,
166
+ );
167
+ // unref: the wait must never be what keeps the process alive — on
168
+ // SIGTERM the gateway drains and exits while this timer is pending
169
+ // (the re-drive/renotify path re-delivers on the next pod).
170
+ await new Promise<void>((resolve) => {
171
+ const timer = setTimeout(resolve, waitMs);
172
+ timer.unref?.();
173
+ });
174
+ }
175
+ if (host.shuttingDown) return;
176
+ }
177
+ // An ack callback that returns void (legacy custom acks) counts as success;
178
+ // an explicit false (ackDispatchEvent's failed HTTP ack) is a failed
179
+ // consume — clearing backoff there would let an ack outage re-create the
180
+ // wire-speed release/re-drive loop against a pre-budget server.
181
+ const settleAck = (ackResult: boolean | void) => ackResult !== false;
182
+ const settle = (acked: boolean) => {
183
+ if (acked) {
184
+ host.typedRedriveBackoff.delete(backoffKey);
185
+ return;
186
+ }
187
+ const failures = (host.typedRedriveBackoff.get(backoffKey)?.failures ?? 0) + 1;
188
+ const backoffMs = Math.min(TYPED_BACKOFF_CAP_MS, TYPED_BACKOFF_BASE_MS * 2 ** (failures - 1));
189
+ // True LRU: delete-then-set moves an updated key to the tail of the
190
+ // Map's insertion order, so capacity eviction always removes the
191
+ // least-recently-FAILING key — an actively-failing old key must not be
192
+ // evicted ahead of a quieter newer one. Evict only when inserting a new
193
+ // key at capacity (an in-place update never shrinks the map).
194
+ if (
195
+ host.typedRedriveBackoff.delete(backoffKey) === false &&
196
+ host.typedRedriveBackoff.size >= TYPED_BACKOFF_MAP_CAP
197
+ ) {
198
+ const oldest = host.typedRedriveBackoff.keys().next().value;
199
+ if (oldest !== undefined) host.typedRedriveBackoff.delete(oldest);
200
+ }
201
+ host.typedRedriveBackoff.set(backoffKey, { failures, until: Date.now() + backoffMs });
202
+ };
203
+
204
+ // Legacy run+ack (no ledger): same settle-in-finally contract as the lane
205
+ // path — a thrown run/ack must arm backoff, not skip it.
206
+ const runLegacy = async () => {
207
+ let acked = false;
208
+ try {
209
+ if (await run(ref.dispatchEventId)) {
210
+ acked = settleAck(await ack(ref.dispatchEventId));
211
+ }
212
+ } finally {
213
+ settle(acked);
214
+ }
215
+ };
216
+
138
217
  if (!host.laneLedger || host.ledgerDisabled) {
139
- if (await run(ref.dispatchEventId)) await ack(ref.dispatchEventId);
218
+ await runLegacy();
140
219
  return;
141
220
  }
142
221
  let lane: Awaited<ReturnType<LaneLedger['claimTyped']>>;
@@ -145,23 +224,29 @@ export async function consumeTypedDispatch(
145
224
  } catch (err) {
146
225
  if (err instanceof LedgerUnsupportedError) {
147
226
  host.disableLedger('claim endpoint missing');
148
- if (await run(ref.dispatchEventId)) await ack(ref.dispatchEventId);
227
+ await runLegacy();
149
228
  return;
150
229
  }
151
230
  throw err;
152
231
  }
153
232
  if (!lane) {
233
+ // Held elsewhere or already resolved — not a local failure; leave the
234
+ // backoff state as-is (a stale entry is cleared by the next local ack).
154
235
  host.opts.log?.info(
155
236
  `typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) — skipping`,
156
237
  );
157
238
  return;
158
239
  }
240
+ let acked = false;
159
241
  try {
160
242
  // Ack must settle before Complete. A fire-and-forget ack races the lane
161
243
  // release: Complete can return the still-received typed item to pending
162
244
  // and publish a re-drive while its successful ack is still in flight.
163
- if (await run(lane.typedDispatchEventId)) await ack(lane.typedDispatchEventId);
245
+ if (await run(lane.typedDispatchEventId)) {
246
+ acked = settleAck(await ack(lane.typedDispatchEventId));
247
+ }
164
248
  } finally {
249
+ settle(acked);
165
250
  // Release the occupancy row. A buffered dispatch may outlive this guard
166
251
  // (lane TTL) — acceptable at-least-once; the ledger's resolution paths
167
252
  // still dedupe the persistent side effects.
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from './provider-config.js';
2
2
  export * from './types.js';
3
3
  export * from './lane-key.js';
4
4
  export type { LaneFlowHost } from './gateway-lane-flow.js';
5
+ export { consumeTypedDispatch } from './gateway-lane-flow.js';
5
6
  export * from './session-state.js';
6
7
  export * from './routing.js';
7
8
  export * from './event-format.js';
@@ -11,6 +12,8 @@ export * from './dispatch-adapter.js';
11
12
  export { createLogger, childLogger } from './logger.js';
12
13
  export * from './gateway-base.js';
13
14
  export * from './platform-config.js';
15
+ export * from './channel-capability.js';
16
+ export * from './channel-token.js';
14
17
  export { writeSkillFiles, buildSkillReferences, SKILLS } from './skills/index.js';
15
18
  export type { SkillMeta } from './skills/index.js';
16
19
  export {
@@ -17,6 +17,46 @@ export interface PlatformConfigManager {
17
17
  fetch(): Promise<PlatformDefaults>;
18
18
  current(): PlatformDefaults;
19
19
  rawConfig(): Record<string, unknown> | null;
20
+ /**
21
+ * Channel capabilities delivered in `agents.capabilities[]` — the
22
+ * declaration plane of the channel-capability broker. Reads the current
23
+ * (possibly LKG-cached) config with NO freshness gate: the credential is
24
+ * pull-at-use (the mint endpoint re-evaluates the grant on every call), so
25
+ * a stale declaration fail-closes there with a self-explanatory 403 — like
26
+ * a stale model, it is suboptimal, never dangerous.
27
+ */
28
+ capabilities(): AgentCapability[];
29
+ }
30
+
31
+ // One platform-granted capability (agents.capabilities[] in platform-config).
32
+ // `fragment` is the complete system-prompt declaration text; the server is
33
+ // the SSOT for channel knowledge, runtimes just splice it in.
34
+ export interface AgentCapability {
35
+ key: string;
36
+ source: string;
37
+ fragment: string;
38
+ }
39
+
40
+ // Defensive extraction: entries missing a non-empty string key/fragment are
41
+ // skipped so a malformed or future-shaped payload can never inject a blank
42
+ // declaration into a system prompt.
43
+ export function extractCapabilities(config: Record<string, unknown>): AgentCapability[] {
44
+ const agents = (config.agents ?? {}) as Record<string, unknown>;
45
+ const raw = agents.capabilities;
46
+ if (!Array.isArray(raw)) return [];
47
+ const out: AgentCapability[] = [];
48
+ for (const entry of raw) {
49
+ if (typeof entry !== 'object' || entry === null) continue;
50
+ const e = entry as Record<string, unknown>;
51
+ if (typeof e.key !== 'string' || !e.key) continue;
52
+ if (typeof e.fragment !== 'string' || !e.fragment) continue;
53
+ out.push({
54
+ key: e.key,
55
+ source: typeof e.source === 'string' ? e.source : '',
56
+ fragment: e.fragment,
57
+ });
58
+ }
59
+ return out;
20
60
  }
21
61
 
22
62
  export interface PlatformManagementProfile {
@@ -276,5 +316,9 @@ export function createPlatformConfigManager(opts: {
276
316
  rawConfig(): Record<string, unknown> | null {
277
317
  return currentRawConfig;
278
318
  },
319
+
320
+ capabilities(): AgentCapability[] {
321
+ return currentRawConfig ? extractCapabilities(currentRawConfig) : [];
322
+ },
279
323
  };
280
324
  }
@@ -212,10 +212,10 @@ resolves and renders the entity title automatically.
212
212
 
213
213
  prll://usr_xxx user prll://prj_xxx project
214
214
  prll://tsk_xxx task prll://wik_xxx wiki
215
- prll://msg_xxx message prll://tcm_xxx task comment
216
- prll://cht_xxx chat prll://ase_xxx agent session
217
- prll://att_xxx attachment prll://sch_xxx schedule
218
- prll://srn_xxx schedule run
215
+ prll://msg_xxx message prll://cmt_xxx comment
216
+ prll://cht_xxx chat prll://tcm_xxx task comment (legacy)
217
+ prll://att_xxx attachment prll://ase_xxx agent session
218
+ prll://sch_xxx schedule prll://srn_xxx schedule run
219
219
 
220
220
  **Wiki** — path is file path, fragment is a typed anchor:
221
221
 
@@ -261,15 +261,37 @@ session already has continuity, so skip the fetch unless something is unclear.
261
261
 
262
262
  Same pattern for any other entity referenced in the event: \`tasks get\`,
263
263
  \`projects get\`, \`users get\`, \`chats get\`. Follow the reflink, don't ask.
264
+ When one entity isn't enough — you need what's *around* it — walk the
265
+ reference graph instead of guessing (see "Walk the reference graph" below).
264
266
 
265
267
  ### Find context with search first
266
268
 
267
269
  Reach for unified semantic search before paging chat history:
268
270
 
269
- parall search "pricing decision june" [--limit 10]
271
+ parall search "pricing decision june" --limit 10
270
272
 
271
- It spans messages, tasks, and wiki. Page \`messages list\` only for the verbatim
272
- recent flow of one chat, not for discovery.
273
+ It spans messages, tasks, wiki, and comments. Page \`messages list\` only for the
274
+ verbatim recent flow of one chat, not for discovery.
275
+
276
+ ### Walk the reference graph
277
+
278
+ References form a traversable graph, and you can query it — don't stop at
279
+ fetching entities one by one:
280
+
281
+ # entity metadata (title, status, preview)
282
+ parall refs resolve prll://tsk_xxx prll://wik_xxx
283
+ # who references this entity
284
+ parall refs backlinks prll://tsk_xxx
285
+ # connected sub-graph around it
286
+ parall refs graph prll://tsk_xxx --depth 2
287
+
288
+ Use \`refs backlinks\` when you need "where is this discussed / used"; use
289
+ \`refs graph\` when you need the full picture around an entity (related tasks,
290
+ docs, conversations — edges carry the author's annotation for why they linked).
291
+ Then \`refs resolve\` the interesting node URIs in one batch to get titles and
292
+ status. \`refs graph\` takes entity-level URIs only (\`prll://wik_xxx\`, not
293
+ \`prll://wik_xxx/docs/a.md\`). All results are filtered to what you can see.
294
+ Details: parall-platform skill.
273
295
 
274
296
  ### File attachments
275
297
 
@@ -21,7 +21,7 @@ export const SKILLS: SkillMeta[] = [
21
21
  {
22
22
  name: 'parall-platform',
23
23
  description:
24
- "Parall platform queries and lightweight agent provisioning: list org members, agents, chats, read message history, check identity, or create another agent. Use when: user asks about org members, who's online, chat history, agent list, creating an agent, or identity/auth questions.",
24
+ "Parall platform queries and lightweight agent provisioning: list org members, agents, chats, read message history, check identity, create another agent, or walk the prll:// reference graph (resolve URIs, backlinks, multi-hop graph). Use when: user asks about org members, who's online, chat history, agent list, creating an agent, identity/auth questions, or you need to find what an entity is connected to / who references it.",
25
25
  content: PARALL_PLATFORM_SKILL,
26
26
  },
27
27
  {
@@ -76,11 +76,11 @@ settled questions or repeat known mistakes. This searches live org data
76
76
  can see.
77
77
 
78
78
  \`\`\`bash
79
- # Semantic + keyword search across messages, tasks, and wiki
79
+ # Semantic + keyword search across messages, tasks, wiki, and comments
80
80
  parall search "auth v5 upgrade"
81
81
 
82
- # Restrict entity types (m=message, t=task, w=wiki). --channel narrows the
83
- # MESSAGE hits to one chat (tasks/wiki are unaffected by it).
82
+ # Restrict entity types (m=message, t=task, w=wiki, c=comment). --channel
83
+ # narrows the MESSAGE hits to one chat (tasks/wiki/comments are unaffected).
84
84
  parall search "auth v5 upgrade" --types m,w --channel prll://cht_eng
85
85
 
86
86
  # Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;
@@ -201,6 +201,8 @@ Every entity is addressable with a \`prll://\` URI. Common prefixes you'll see i
201
201
  | \`prll://usr_\` | User (human or agent) | parall-platform |
202
202
  | \`prll://cht_\` | Chat | parall-platform |
203
203
  | \`prll://msg_\` | Message | parall-platform |
204
+ | \`prll://cmt_\` | Comment (on tasks, wiki pages, changesets) | by target: task comment → parall-tasks, wiki/changeset comment → parall-wiki |
205
+ | \`prll://ase_\` | Agent session | parall-platform |
204
206
  | \`prll://tsk_\` | Task | parall-tasks |
205
207
  | \`prll://prj_\` | Project | parall-tasks |
206
208
  | \`prll://sch_\` | Schedule (time trigger) | parall-schedules |
@@ -236,7 +238,24 @@ parall refs graph prll://tsk_xxx --depth 2
236
238
 
237
239
  \`refs graph\` traverses both directions (inbound + outbound) and returns \`nodes\`
238
240
  and \`edges\` with each node's hop \`depth\`. \`truncated: true\` means a size cap clipped
239
- the result — narrow it with a smaller \`--depth\`.
241
+ the result — narrow it with a smaller \`--depth\`. Edges carry \`context\` — the
242
+ author's annotation from \`[context](prll://...)\` — telling you *why* two
243
+ entities are linked, not just that they are.
244
+
245
+ The graph returns bare node URIs (no titles). The usual two-step: \`refs graph\`
246
+ for topology, then batch-\`refs resolve\` the node URIs you care about for
247
+ titles/status. If graph rejects your URI with a path/anchor error, strip it to
248
+ the entity root (\`prll://wik_xxx/docs/a.md\` → \`prll://wik_xxx\`) and re-query —
249
+ but note this WIDENS the query to the whole entity, not that one file: the
250
+ graph seeds from the wiki id, so a specific file's outbound links may sit
251
+ deeper in the result (or past the size caps). For refs pointing AT one file
252
+ (inbound), \`refs backlinks\` on the full file URI is precise. There is no
253
+ precise query for one file's OUTBOUND edges today — the widened root graph is
254
+ best-effort for those, or read the file itself for its \`prll://\` links.
255
+ Wiki-file nodes inside a graph *result* do legitimately carry paths.
256
+
257
+ \`refs backlinks\` items include a \`snippet\` of the referencing content — often
258
+ enough to judge relevance without fetching the source entity.
240
259
 
241
260
  CLI success output is JSON. Errors print a JSON line (\`{"error","status","code",...}\`) and, on a \`PERMISSION_DENIED\`, may add a plain-text \`Request approval:\` line — read both.
242
261
  `;
package/src/types.ts CHANGED
@@ -69,10 +69,14 @@ export type ParallEvent = {
69
69
  /** External IM channel metadata, used for channel_message events. */
70
70
  channelProvider?: string;
71
71
  channelConversationType?: string;
72
- /** Provider-side conversation id (the send_message target). */
72
+ /** Provider-side conversation id (the reply's addressing target). */
73
73
  channelExternalConversationId?: string;
74
74
  /** Provider-side message id (in-thread reply target). */
75
75
  channelExternalMessageId?: string;
76
+ /** Live capability grant: the `<provider>-cli` capability is active, so
77
+ * the vendor CLI (broker shim) is on PATH and is THE reply path. False /
78
+ * absent → outbound is disabled for this org; the hint says so. */
79
+ channelCliCapable?: boolean;
76
80
  /** Original event timestamp (e.g., message.created_at). When present,
77
81
  * input steps use this instead of server insertion time for ordering. */
78
82
  sentAt?: string;