@parall/agent-core 1.20.2 → 1.22.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.
@@ -1 +1 @@
1
- {"version":3,"file":"event-format.d.ts","sourceRoot":"","sources":["../src/event-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAwCzD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CASnE"}
1
+ {"version":3,"file":"event-format.d.ts","sourceRoot":"","sources":["../src/event-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CA+CzD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CASnE"}
@@ -35,6 +35,16 @@ export function buildEventBody(event) {
35
35
  lines.push(`[Comment ID: prll://${event.messageId}]`);
36
36
  lines.push("", event.body);
37
37
  }
38
+ else if (event.type === "schedule") {
39
+ lines.push(`[Event: schedule.fired]`);
40
+ lines.push(`[Schedule: prll://${event.targetId}]`);
41
+ lines.push(`[Run: prll://${event.messageId}]`);
42
+ if (event.scheduledFireAt)
43
+ lines.push(`[Scheduled at: ${sanitizeMeta(event.scheduledFireAt)}]`);
44
+ if (event.attachedUri)
45
+ lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
46
+ lines.push("", event.body);
47
+ }
38
48
  else {
39
49
  lines.push(`[Event: task.assigned]`);
40
50
  const taskLabel = event.targetName
@@ -69,6 +69,16 @@ export declare class ParallAgentGateway {
69
69
  private handleMessage;
70
70
  private handleTaskAssignment;
71
71
  private handleTaskComment;
72
+ /**
73
+ * Fetch the schedule_run snapshot, then feed it as an inbound event. 404 is
74
+ * tolerated and treated as "dispatched" so the caller can ack the stale
75
+ * delivery — see `docs/primitives/schedule.md` § "Snapshot 不变性: 内容 vs
76
+ * 可见性". A later PATCH on target_id / attached_to_uri legitimately revokes
77
+ * access to already-delivered run snapshots, and the runtime must not crash
78
+ * or retry forever in that case.
79
+ */
80
+ private fetchAndHandleScheduleFire;
81
+ private handleScheduleFire;
72
82
  private catchUpFromDispatch;
73
83
  private handleHello;
74
84
  private waitForDrain;
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"AAIA,OAAO,EAAuB,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,KAAK,EACV,qBAAqB,EAWtB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAEV,eAAe,EAGf,aAAa,EAEd,MAAM,uBAAuB,CAAC;AAoD/B,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,EAAE,EAAE,QAAQ,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,eAAe,EAAE,eAAe,CAAC;IACjC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAI3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACtE,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACjH,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACjD,CAAC;AAUF,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKnF;AAqCD,qBAAa,kBAAkB;IAmCjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAlCjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqC;IACtE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsC;IACjE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAK5B;IAEF,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAS;IAMzB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,cAAc,CAAyB;IAE/C,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAQ;IAChD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAI9C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;gBAEjB,IAAI,EAAE,oBAAoB;IAKjD,GAAG,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IA+GlD,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,oBAAoB;YAmBd,eAAe;YA6Bf,iBAAiB;IAsF/B,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,eAAe;YAQT,gCAAgC;YAUhC,WAAW;YAoFX,gBAAgB;YAkFhB,eAAe;YA6Ef,kBAAkB;YA2ElB,kBAAkB;YAkBlB,4BAA4B;YAoD5B,aAAa;YA+Bb,oBAAoB;YAoCpB,iBAAiB;IA2E/B;;;;;;;OAOG;YACW,0BAA0B;YAsB1B,kBAAkB;YAsClB,mBAAmB;YA6GnB,WAAW;IA6DzB,OAAO,CAAC,YAAY;YAiBN,QAAQ;CA4CvB"}
@@ -29,6 +29,9 @@ function resolveStepTarget(event) {
29
29
  if (event.targetId.startsWith("cht_")) {
30
30
  return { target_type: "chat", target_id: event.targetId };
31
31
  }
32
+ if (event.type === "schedule" || event.targetId.startsWith("sch_")) {
33
+ return { target_type: "schedule", target_id: event.targetId };
34
+ }
32
35
  return { target_type: "", target_id: event.targetId || undefined };
33
36
  }
34
37
  async function fetchAllChats(client, orgId, chatInfoMap) {
@@ -150,18 +153,41 @@ export class ParallAgentGateway {
150
153
  }
151
154
  });
152
155
  ws.on("dispatch.new", async (data) => {
153
- if (data.event_type !== "task_comment")
154
- return;
155
- if (!data.source_id || !data.task_id)
156
- return;
157
- try {
158
- const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id);
159
- if (dispatched) {
160
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
156
+ if (data.event_type === "task_comment") {
157
+ if (!data.source_id || !data.task_id)
158
+ return;
159
+ try {
160
+ const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id);
161
+ if (dispatched) {
162
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
163
+ }
164
+ }
165
+ catch (err) {
166
+ this.opts.log?.error(`parall[${this.opts.accountId}]: task comment dispatch failed for ${data.source_id}: ${String(err)}`);
161
167
  }
162
168
  }
163
- catch (err) {
164
- this.opts.log?.error(`parall[${this.opts.accountId}]: task comment dispatch failed for ${data.source_id}: ${String(err)}`);
169
+ else if (data.event_type === "schedule.fire") {
170
+ if (!data.source_id)
171
+ return;
172
+ try {
173
+ const dispatched = await this.fetchAndHandleScheduleFire(data.source_id, data.actor_id);
174
+ if (dispatched) {
175
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
176
+ }
177
+ }
178
+ catch (err) {
179
+ this.opts.log?.error(`parall[${this.opts.accountId}]: schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
180
+ }
181
+ }
182
+ else if (data.event_type !== "message" && data.event_type !== "task_assign") {
183
+ // Truly unknown event_type — log so a newly-added dispatch type
184
+ // not yet wired here surfaces during runtime testing. "message"
185
+ // and "task_assign" are deliberately excluded: dispatch.new
186
+ // carries them too, but they are owned by dedicated WS handlers
187
+ // (message.new, task.assigned) above and would otherwise spam
188
+ // info-level logs for every inbound chat message / task
189
+ // assignment on a busy agent.
190
+ this.opts.log?.info(`parall[${this.opts.accountId}]: dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) — no-op`);
165
191
  }
166
192
  });
167
193
  this.opts.log?.info(`parall[${this.opts.accountId}]: connecting to ${this.opts.connectionLabel ?? "Parall WS"}...`);
@@ -241,8 +267,14 @@ export class ParallAgentGateway {
241
267
  target_type: target.target_type,
242
268
  target_id: target.target_id,
243
269
  content: {
244
- trigger_type: event.type === "task" ? "task_assign" : event.type === "task_comment" ? "task_comment" : "mention",
245
- trigger_ref: event.type === "task" ? { task_id: event.targetId } : event.type === "task_comment" ? { comment_id: event.messageId, task_id: event.targetId } : { message_id: event.messageId },
270
+ trigger_type: event.type === "task" ? "task_assign" :
271
+ event.type === "task_comment" ? "task_comment" :
272
+ event.type === "schedule" ? "schedule_fire" :
273
+ "mention",
274
+ trigger_ref: event.type === "task" ? { task_id: event.targetId } :
275
+ event.type === "task_comment" ? { comment_id: event.messageId, task_id: event.targetId } :
276
+ event.type === "schedule" ? { schedule_id: event.targetId, run_id: event.messageId } :
277
+ { message_id: event.messageId },
246
278
  sender_id: event.senderId,
247
279
  sender_name: event.senderName,
248
280
  summary: event.body.substring(0, 200),
@@ -870,6 +902,75 @@ export class ParallAgentGateway {
870
902
  }
871
903
  return dispatched;
872
904
  }
905
+ /**
906
+ * Fetch the schedule_run snapshot, then feed it as an inbound event. 404 is
907
+ * tolerated and treated as "dispatched" so the caller can ack the stale
908
+ * delivery — see `docs/primitives/schedule.md` § "Snapshot 不变性: 内容 vs
909
+ * 可见性". A later PATCH on target_id / attached_to_uri legitimately revokes
910
+ * access to already-delivered run snapshots, and the runtime must not crash
911
+ * or retry forever in that case.
912
+ */
913
+ async fetchAndHandleScheduleFire(runId, actorId) {
914
+ let run = null;
915
+ try {
916
+ run = await this.opts.client.getScheduleRun(this.opts.config.org_id, runId);
917
+ }
918
+ catch (err) {
919
+ const status = err?.status;
920
+ if (status === 404) {
921
+ // Contract: ack + warn log + drop. A post-fire PATCH on
922
+ // target_id / attached_to_uri can legitimately revoke access to an
923
+ // already-delivered run — this is expected but operationally
924
+ // visible, so warn (not info) keeps it findable during incident
925
+ // triage. See docs/primitives/schedule.md § Snapshot 不变性.
926
+ this.opts.log?.warn(`parall[${this.opts.accountId}]: schedule run ${runId} not accessible (404), acking stale dispatch`);
927
+ return true;
928
+ }
929
+ this.opts.log?.warn(`parall[${this.opts.accountId}]: schedule run fetch failed for ${runId}, leaving pending: ${String(err)}`);
930
+ return false;
931
+ }
932
+ if (!run)
933
+ return true;
934
+ return this.handleScheduleFire(run, actorId);
935
+ }
936
+ async handleScheduleFire(run, actorId) {
937
+ if (this.shuttingDown)
938
+ return false; // drain window — let server requeue via catch-up
939
+ const dedupeKey = `schedule_run:${run.id}`;
940
+ if (this.dispatchedTasks.has(dedupeKey))
941
+ return false;
942
+ this.dispatchedTasks.add(dedupeKey);
943
+ this.opts.log?.info(`parall[${this.opts.accountId}]: schedule fired: ${run.id} (schedule ${run.schedule_id})`);
944
+ const event = {
945
+ type: "schedule",
946
+ // Route by schedule_id (not attached chat_id) so concurrent fires of
947
+ // different schedules can fork independently — matches the PR1 primitive
948
+ // design where "schedule triggers; target decides response" and fire
949
+ // semantics are independent of any attached conversation.
950
+ targetId: run.schedule_id,
951
+ targetType: "schedule",
952
+ senderId: actorId ?? "system",
953
+ senderName: "schedule",
954
+ messageId: run.id,
955
+ body: run.fired_description ?? "",
956
+ scheduledFireAt: run.scheduled_fire_at,
957
+ attachedUri: run.fired_attached_uri ?? undefined,
958
+ ackSourceType: "schedule_run",
959
+ ackSourceId: run.id,
960
+ };
961
+ let dispatched;
962
+ try {
963
+ dispatched = await this.handleInboundEvent(event);
964
+ }
965
+ catch (err) {
966
+ this.dispatchedTasks.delete(dedupeKey);
967
+ throw err;
968
+ }
969
+ if (!dispatched) {
970
+ this.dispatchedTasks.delete(dedupeKey);
971
+ }
972
+ return dispatched;
973
+ }
873
974
  async catchUpFromDispatch(coldStart = false) {
874
975
  const minAge = coldStart ? Date.now() - this.COLD_START_WINDOW_MS : 0;
875
976
  let cursor;
@@ -926,6 +1027,9 @@ export class ParallAgentGateway {
926
1027
  else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
927
1028
  dispatched = await this.handleTaskComment(item.source_id, item.task_id, item.actor_id);
928
1029
  }
1030
+ else if (item.event_type === "schedule.fire" && item.source_id) {
1031
+ dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
1032
+ }
929
1033
  else if (item.event_type === "message" && item.source_id && item.chat_id) {
930
1034
  if (!this.tryClaimMessage(item.source_id))
931
1035
  continue;
@@ -9,7 +9,14 @@
9
9
  * audiences, no duplication — this teaches agents HOW to work on Parall, the
10
10
  * design docs explain WHY the system works the way it does.
11
11
  */
12
+ export interface AgentIdentity {
13
+ userId: string;
14
+ displayName: string;
15
+ description?: string;
16
+ }
17
+ export declare function buildIdentity(agent?: AgentIdentity): string;
18
+ /** @deprecated Use buildIdentity() instead. Kept for backward compat during migration. */
12
19
  export declare const PRLL_IDENTITY = "## You on Parall\n\nParall is a shared workspace where humans and agents work side by side as equals.\nYou are a participant here, not a service. You hold tasks, own decisions, and are\naccountable for the work you take on \u2014 the same way a human teammate is.\n\nThe people and agents around you are collaborators, not users to serve. Be honest,\nbe direct, and care about the outcome of the work \u2014 not just the request in front\nof you.";
13
20
  export declare const PRLL_BEHAVIOR = "## How to work here\n\n### Move work forward\nDon't wait for instructions. If you see the next step, take it. If something is\nambiguous, clarify once and proceed. If you're blocked, say what's blocking you\n\u2014 don't go silent. Initiative is expected.\n\n### Work in the open\nNothing you do exists until the system can see it. Your progress, decisions,\nblockers, and results need to live in tasks, comments, messages, or wiki pages\n\u2014 otherwise the organization is blind to your work, and so is the next agent\nwho picks up where you left off. Leave traces as you go, not at the end.\n\nFor non-trivial work: create or claim a task, mark it `in_progress`, comment\nwhen status materially changes, close it when done, and link the origin that\ntriggered it. Details: load the `parall-tasks` skill.\n\n### Communicate like a teammate\nMatch the conversation \u2014 concise in chat, thorough in docs, plain language over\njargon. Say what matters; stop when you're done. Don't narrate every tool call\nor pad replies to seem thorough.\n\n### Respect what's shared\nYou have broad latitude inside your own work. But actions that are visible to\nothers, hard to reverse, or touch shared state \u2014 sending DMs, editing shared\nwiki, reassigning others' tasks, deleting content \u2014 pause and confirm before\nacting, unless you've been explicitly authorized.\n\n### Shared workspace\nOther agents share this workspace. Before starting work, check whether someone\n\u2014 human or agent \u2014 has already picked it up. Coordination beats racing.\n\n### When in doubt\nPrefer asking over guessing. Prefer \"I don't know\" over fabricating. Your\ncredibility is what you bring to the workspace \u2014 protect it.";
14
- export declare const PRLL_REFERENCE_GUIDE = "## Parall References\n\nEvery entity on Parall has a `prll://` URI. Use these URIs to link related\nentities when you create or update tasks, comments, messages, and wiki files.\n\nAll three forms work \u2014 pick whichever fits:\n\n prll://tsk_abc bare URI (auto-linked)\n [](prll://tsk_abc) empty context (renders resolved title)\n [relevant context](prll://tsk_abc) with author annotation\n\nBare URIs and empty-context refs are preferred in most cases \u2014 the platform\nresolves and renders the entity title automatically.\n\n### URI format\n\n`prll://` follows standard URI structure: `scheme://authority/path?query#fragment`.\n\n**Entities** \u2014 the entity ID is the authority:\n\n prll://usr_xxx user prll://prj_xxx project\n prll://tsk_xxx task prll://wik_xxx wiki\n prll://msg_xxx message prll://tcm_xxx task comment\n prll://cht_xxx chat prll://ase_xxx agent session\n prll://att_xxx attachment\n\n**Wiki** \u2014 path is file path, fragment is a typed anchor:\n\n prll://wik_xxx/docs/guide.md file\n prll://wik_xxx/docs/guide.md#h=Auth::OAuth heading (:: = hierarchy)\n prll://wik_xxx/src/auth.go?rev=<sha>#l=42-58 line range (revision-pinned)\n\n Anchor types: `h=` heading, `l=` line/range, `s=` symbol.\n Line anchors in persistent content require `?rev=<full-40-char-sha>`.\n\n**Chat message range**:\n\n prll://cht_xxx#range=msg_01HA,msg_01HZ\n\n**Field access** \u2014 path selects a field (omit to reference the entity itself):\n\n prll://tsk_xxx/description#Implementation heading within task description\n\n### Reading context on demand\n\nAn event only carries the single triggering message. If you're mentioned in a\ngroup chat and lack context, pull what you need from the chat \u2014 don't guess:\n\n npx --yes @parall/cli@latest messages list cht_xxx --limit 20 --before msg_xxx\n npx --yes @parall/cli@latest messages get msg_xxx\n npx --yes @parall/cli@latest chats get cht_xxx\n\nRule of thumb: in a group chat mention, the conversation that led up to you\nbeing called almost always matters \u2014 read it before replying. In a DM, your\nsession already has continuity, so skip the fetch unless something is unclear.\n\nSame pattern for any other entity referenced in the event: `tasks get`,\n`projects get`, `users get`, `chats get`. Follow the reflink, don't ask.\n\n### File attachments\n\nMessages may include attachments. They appear in events as:\n\n [Attachment: prll://att_xxx | image/png | 1.2MB | screenshot.png]\n\nTo download an attachment, use the CLI:\n\n npx @parall/cli@latest files download att_xxx --output /tmp/screenshot.png\n\nTo send a file:\n\n npx @parall/cli@latest messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\nOr upload first and reuse across chats:\n\n npx @parall/cli@latest files upload /tmp/report.pdf\n npx @parall/cli@latest messages send prll://cht_aaa --attachment att_yyy --text \"Report\"\n npx @parall/cli@latest messages send prll://cht_bbb --attachment att_yyy --text \"FYI\"\n\n### When to reference\n\n- **Origin** \u2014 always link the message or task that triggered your work\n- **Design docs / wiki** \u2014 link specs and guides relevant to the work\n- **Related tasks** \u2014 link parent, sibling, or blocking tasks\n- **People** \u2014 link assignees or stakeholders when mentioning them\n- **Conversations** \u2014 link a chat or message range as context\n\n### Why this matters\n\nOther agents and humans read your output. References build a navigable context graph \u2014\nin multi-agent workflows, your references are the map that the next agent follows.";
21
+ export declare const PRLL_REFERENCE_GUIDE = "## Parall References\n\nEvery entity on Parall has a `prll://` URI. Use these URIs to link related\nentities when you create or update tasks, comments, messages, and wiki files.\n\nAll three forms work \u2014 pick whichever fits:\n\n prll://tsk_abc bare URI (auto-linked)\n [](prll://tsk_abc) empty context (renders resolved title)\n [relevant context](prll://tsk_abc) with author annotation\n\nBare URIs and empty-context refs are preferred in most cases \u2014 the platform\nresolves and renders the entity title automatically.\n\n### URI format\n\n`prll://` follows standard URI structure: `scheme://authority/path?query#fragment`.\n\n**Entities** \u2014 the entity ID is the authority:\n\n prll://usr_xxx user prll://prj_xxx project\n prll://tsk_xxx task prll://wik_xxx wiki\n prll://msg_xxx message prll://tcm_xxx task comment\n prll://cht_xxx chat prll://ase_xxx agent session\n prll://att_xxx attachment prll://sch_xxx schedule\n prll://srn_xxx schedule run\n\n**Wiki** \u2014 path is file path, fragment is a typed anchor:\n\n prll://wik_xxx/docs/guide.md file\n prll://wik_xxx/docs/guide.md#h=Auth::OAuth heading (:: = hierarchy)\n prll://wik_xxx/src/auth.go?rev=<sha>#l=42-58 line range (revision-pinned)\n\n Anchor types: `h=` heading, `l=` line/range, `s=` symbol.\n Line anchors in persistent content require `?rev=<full-40-char-sha>`.\n\n**Chat message range**:\n\n prll://cht_xxx#range=msg_01HA,msg_01HZ\n\n**Field access** \u2014 path selects a field (omit to reference the entity itself):\n\n prll://tsk_xxx/description#Implementation heading within task description\n\n### Reading context on demand\n\nAn event only carries the single triggering message. If you're mentioned in a\ngroup chat and lack context, pull what you need from the chat \u2014 don't guess:\n\n npx --yes @parall/cli@latest messages list cht_xxx --limit 20 --before msg_xxx\n npx --yes @parall/cli@latest messages get msg_xxx\n npx --yes @parall/cli@latest chats get cht_xxx\n\nRule of thumb: in a group chat mention, the conversation that led up to you\nbeing called almost always matters \u2014 read it before replying. In a DM, your\nsession already has continuity, so skip the fetch unless something is unclear.\n\nSame pattern for any other entity referenced in the event: `tasks get`,\n`projects get`, `users get`, `chats get`. Follow the reflink, don't ask.\n\n### File attachments\n\nMessages may include attachments. They appear in events as:\n\n [Attachment: prll://att_xxx | image/png | 1.2MB | screenshot.png]\n\nTo download an attachment, use the CLI:\n\n npx @parall/cli@latest files download att_xxx --output /tmp/screenshot.png\n\nTo send a file:\n\n npx @parall/cli@latest messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\nOr upload first and reuse across chats:\n\n npx @parall/cli@latest files upload /tmp/report.pdf\n npx @parall/cli@latest messages send prll://cht_aaa --attachment att_yyy --text \"Report\"\n npx @parall/cli@latest messages send prll://cht_bbb --attachment att_yyy --text \"FYI\"\n\n### When to reference\n\n- **Origin** \u2014 always link the message or task that triggered your work\n- **Design docs / wiki** \u2014 link specs and guides relevant to the work\n- **Related tasks** \u2014 link parent, sibling, or blocking tasks\n- **People** \u2014 link assignees or stakeholders when mentioning them\n- **Conversations** \u2014 link a chat or message range as context\n\n### Why this matters\n\nOther agents and humans read your output. References build a navigable context graph \u2014\nin multi-agent workflows, your references are the map that the next agent follows.";
15
22
  //# sourceMappingURL=prompt-fragments.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-fragments.d.ts","sourceRoot":"","sources":["../src/prompt-fragments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,aAAa,mcAQlB,CAAC;AAET,eAAO,MAAM,aAAa,4rDAkCmC,CAAC;AAE9D,eAAO,MAAM,oBAAoB,swHA0FkD,CAAC"}
1
+ {"version":3,"file":"prompt-fragments.d.ts","sourceRoot":"","sources":["../src/prompt-fragments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAgBD,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,GAAG,MAAM,CAa3D;AAED,0FAA0F;AAC1F,eAAO,MAAM,aAAa,mcAAqB,CAAC;AAEhD,eAAO,MAAM,aAAa,4rDAkCmC,CAAC;AAE9D,eAAO,MAAM,oBAAoB,03HA2FkD,CAAC"}
@@ -9,7 +9,7 @@
9
9
  * audiences, no duplication — this teaches agents HOW to work on Parall, the
10
10
  * design docs explain WHY the system works the way it does.
11
11
  */
12
- export const PRLL_IDENTITY = `## You on Parall
12
+ const PRLL_IDENTITY_BASE = `## You on Parall
13
13
 
14
14
  Parall is a shared workspace where humans and agents work side by side as equals.
15
15
  You are a participant here, not a service. You hold tasks, own decisions, and are
@@ -18,6 +18,23 @@ accountable for the work you take on — the same way a human teammate is.
18
18
  The people and agents around you are collaborators, not users to serve. Be honest,
19
19
  be direct, and care about the outcome of the work — not just the request in front
20
20
  of you.`;
21
+ function sanitizeProfileField(value) {
22
+ return value.replace(/[\r\n]+/g, " ").replace(/`/g, "'").trim();
23
+ }
24
+ export function buildIdentity(agent) {
25
+ if (!agent)
26
+ return PRLL_IDENTITY_BASE;
27
+ const name = sanitizeProfileField(agent.displayName);
28
+ const lines = [PRLL_IDENTITY_BASE, "", "### Your Parall Identity", ""];
29
+ lines.push(`You are **${name}** (\`prll://${agent.userId}\`).`);
30
+ if (agent.description) {
31
+ lines.push(`Description: ${sanitizeProfileField(agent.description)}`);
32
+ }
33
+ lines.push("", `When you see \`${agent.userId}\` or \`prll://${agent.userId}\` in messages, mentions, or events — that's you.`);
34
+ return lines.join("\n");
35
+ }
36
+ /** @deprecated Use buildIdentity() instead. Kept for backward compat during migration. */
37
+ export const PRLL_IDENTITY = PRLL_IDENTITY_BASE;
21
38
  export const PRLL_BEHAVIOR = `## How to work here
22
39
 
23
40
  ### Move work forward
@@ -77,7 +94,8 @@ resolves and renders the entity title automatically.
77
94
  prll://tsk_xxx task prll://wik_xxx wiki
78
95
  prll://msg_xxx message prll://tcm_xxx task comment
79
96
  prll://cht_xxx chat prll://ase_xxx agent session
80
- prll://att_xxx attachment
97
+ prll://att_xxx attachment prll://sch_xxx schedule
98
+ prll://srn_xxx schedule run
81
99
 
82
100
  **Wiki** — path is file path, fragment is a typed anchor:
83
101
 
package/dist/types.d.ts CHANGED
@@ -18,7 +18,7 @@ export type DispatchState = {
18
18
  };
19
19
  /** Normalized inbound event from Parall. */
20
20
  export type ParallEvent = {
21
- type: "message" | "task" | "task_comment";
21
+ type: "message" | "task" | "task_comment" | "schedule";
22
22
  targetId: string;
23
23
  targetName?: string;
24
24
  targetType?: string;
@@ -34,7 +34,11 @@ export type ParallEvent = {
34
34
  fileSize: number;
35
35
  mimeType: string;
36
36
  }>;
37
- ackSourceType?: "message" | "task_activity" | "comment";
37
+ /** Fire-time snapshot timestamp, used for schedule events. */
38
+ scheduledFireAt?: string;
39
+ /** Fire-time attached_to_uri snapshot, used for schedule events. */
40
+ attachedUri?: string;
41
+ ackSourceType?: "message" | "task_activity" | "comment" | "schedule_run";
38
42
  ackSourceId?: string;
39
43
  };
40
44
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kBAAkB,EAAE,UAAU,EAAE,CAAC;IACjC,UAAU,EAAE,WAAW,EAAE,CAAC;CAC3B,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,aAAa,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG,SAAS,CAAC;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kBAAkB,EAAE,UAAU,EAAE,CAAC;IACjC,UAAU,EAAE,WAAW,EAAE,CAAC;CAC3B,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,cAAc,GAAG,UAAU,CAAC;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG,SAAS,GAAG,cAAc,CAAC;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/agent-core",
3
- "version": "1.20.2",
3
+ "version": "1.22.0",
4
4
  "description": "Shared agent runtime orchestration helpers for Parall",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "src"
23
23
  ],
24
24
  "dependencies": {
25
- "@parall/sdk": "1.20.2"
25
+ "@parall/sdk": "1.22.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.0.0",
@@ -34,6 +34,13 @@ export function buildEventBody(event: ParallEvent): string {
34
34
  lines.push(`[From: ${event.senderName} (prll://${event.senderId})]`);
35
35
  lines.push(`[Comment ID: prll://${event.messageId}]`);
36
36
  lines.push("", event.body);
37
+ } else if (event.type === "schedule") {
38
+ lines.push(`[Event: schedule.fired]`);
39
+ lines.push(`[Schedule: prll://${event.targetId}]`);
40
+ lines.push(`[Run: prll://${event.messageId}]`);
41
+ if (event.scheduledFireAt) lines.push(`[Scheduled at: ${sanitizeMeta(event.scheduledFireAt)}]`);
42
+ if (event.attachedUri) lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
43
+ lines.push("", event.body);
37
44
  } else {
38
45
  lines.push(`[Event: task.assigned]`);
39
46
  const taskLabel = event.targetName
@@ -11,6 +11,7 @@ import type {
11
11
  DispatchNewData,
12
12
  HelloData,
13
13
  MessageNewData,
14
+ ScheduleRun,
14
15
  Task,
15
16
  TaskAssignedData,
16
17
  TextContent,
@@ -124,6 +125,9 @@ function resolveStepTarget(event: ParallEvent): { target_type: string; target_id
124
125
  if (event.targetId.startsWith("cht_")) {
125
126
  return { target_type: "chat", target_id: event.targetId };
126
127
  }
128
+ if (event.type === "schedule" || event.targetId.startsWith("sch_")) {
129
+ return { target_type: "schedule", target_id: event.targetId };
130
+ }
127
131
  return { target_type: "", target_id: event.targetId || undefined };
128
132
  }
129
133
 
@@ -257,15 +261,35 @@ export class ParallAgentGateway {
257
261
  });
258
262
 
259
263
  ws.on("dispatch.new", async (data: DispatchNewData) => {
260
- if (data.event_type !== "task_comment") return;
261
- if (!data.source_id || !data.task_id) return;
262
- try {
263
- const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id);
264
- if (dispatched) {
265
- this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
264
+ if (data.event_type === "task_comment") {
265
+ if (!data.source_id || !data.task_id) return;
266
+ try {
267
+ const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id);
268
+ if (dispatched) {
269
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
270
+ }
271
+ } catch (err) {
272
+ this.opts.log?.error(`parall[${this.opts.accountId}]: task comment dispatch failed for ${data.source_id}: ${String(err)}`);
266
273
  }
267
- } catch (err) {
268
- this.opts.log?.error(`parall[${this.opts.accountId}]: task comment dispatch failed for ${data.source_id}: ${String(err)}`);
274
+ } else if (data.event_type === "schedule.fire") {
275
+ if (!data.source_id) return;
276
+ try {
277
+ const dispatched = await this.fetchAndHandleScheduleFire(data.source_id, data.actor_id);
278
+ if (dispatched) {
279
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
280
+ }
281
+ } catch (err) {
282
+ this.opts.log?.error(`parall[${this.opts.accountId}]: schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
283
+ }
284
+ } else if (data.event_type !== "message" && data.event_type !== "task_assign") {
285
+ // Truly unknown event_type — log so a newly-added dispatch type
286
+ // not yet wired here surfaces during runtime testing. "message"
287
+ // and "task_assign" are deliberately excluded: dispatch.new
288
+ // carries them too, but they are owned by dedicated WS handlers
289
+ // (message.new, task.assigned) above and would otherwise spam
290
+ // info-level logs for every inbound chat message / task
291
+ // assignment on a busy agent.
292
+ this.opts.log?.info(`parall[${this.opts.accountId}]: dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) — no-op`);
269
293
  }
270
294
  });
271
295
 
@@ -346,8 +370,16 @@ export class ParallAgentGateway {
346
370
  target_type: target.target_type,
347
371
  target_id: target.target_id,
348
372
  content: {
349
- trigger_type: event.type === "task" ? "task_assign" : event.type === "task_comment" ? "task_comment" : "mention",
350
- trigger_ref: event.type === "task" ? { task_id: event.targetId } : event.type === "task_comment" ? { comment_id: event.messageId, task_id: event.targetId } : { message_id: event.messageId },
373
+ trigger_type:
374
+ event.type === "task" ? "task_assign" :
375
+ event.type === "task_comment" ? "task_comment" :
376
+ event.type === "schedule" ? "schedule_fire" :
377
+ "mention",
378
+ trigger_ref:
379
+ event.type === "task" ? { task_id: event.targetId } :
380
+ event.type === "task_comment" ? { comment_id: event.messageId, task_id: event.targetId } :
381
+ event.type === "schedule" ? { schedule_id: event.targetId, run_id: event.messageId } :
382
+ { message_id: event.messageId },
351
383
  sender_id: event.senderId,
352
384
  sender_name: event.senderName,
353
385
  summary: event.body.substring(0, 200),
@@ -1001,6 +1033,74 @@ export class ParallAgentGateway {
1001
1033
  return dispatched;
1002
1034
  }
1003
1035
 
1036
+ /**
1037
+ * Fetch the schedule_run snapshot, then feed it as an inbound event. 404 is
1038
+ * tolerated and treated as "dispatched" so the caller can ack the stale
1039
+ * delivery — see `docs/primitives/schedule.md` § "Snapshot 不变性: 内容 vs
1040
+ * 可见性". A later PATCH on target_id / attached_to_uri legitimately revokes
1041
+ * access to already-delivered run snapshots, and the runtime must not crash
1042
+ * or retry forever in that case.
1043
+ */
1044
+ private async fetchAndHandleScheduleFire(runId: string, actorId: string | null): Promise<boolean> {
1045
+ let run: ScheduleRun | null = null;
1046
+ try {
1047
+ run = await this.opts.client.getScheduleRun(this.opts.config.org_id, runId);
1048
+ } catch (err: unknown) {
1049
+ const status = (err as { status?: number })?.status;
1050
+ if (status === 404) {
1051
+ // Contract: ack + warn log + drop. A post-fire PATCH on
1052
+ // target_id / attached_to_uri can legitimately revoke access to an
1053
+ // already-delivered run — this is expected but operationally
1054
+ // visible, so warn (not info) keeps it findable during incident
1055
+ // triage. See docs/primitives/schedule.md § Snapshot 不变性.
1056
+ this.opts.log?.warn(`parall[${this.opts.accountId}]: schedule run ${runId} not accessible (404), acking stale dispatch`);
1057
+ return true;
1058
+ }
1059
+ this.opts.log?.warn(`parall[${this.opts.accountId}]: schedule run fetch failed for ${runId}, leaving pending: ${String(err)}`);
1060
+ return false;
1061
+ }
1062
+ if (!run) return true;
1063
+ return this.handleScheduleFire(run, actorId);
1064
+ }
1065
+
1066
+ private async handleScheduleFire(run: ScheduleRun, actorId: string | null): Promise<boolean> {
1067
+ if (this.shuttingDown) return false; // drain window — let server requeue via catch-up
1068
+ const dedupeKey = `schedule_run:${run.id}`;
1069
+ if (this.dispatchedTasks.has(dedupeKey)) return false;
1070
+ this.dispatchedTasks.add(dedupeKey);
1071
+ this.opts.log?.info(`parall[${this.opts.accountId}]: schedule fired: ${run.id} (schedule ${run.schedule_id})`);
1072
+
1073
+ const event: ParallEvent = {
1074
+ type: "schedule",
1075
+ // Route by schedule_id (not attached chat_id) so concurrent fires of
1076
+ // different schedules can fork independently — matches the PR1 primitive
1077
+ // design where "schedule triggers; target decides response" and fire
1078
+ // semantics are independent of any attached conversation.
1079
+ targetId: run.schedule_id,
1080
+ targetType: "schedule",
1081
+ senderId: actorId ?? "system",
1082
+ senderName: "schedule",
1083
+ messageId: run.id,
1084
+ body: run.fired_description ?? "",
1085
+ scheduledFireAt: run.scheduled_fire_at,
1086
+ attachedUri: run.fired_attached_uri ?? undefined,
1087
+ ackSourceType: "schedule_run",
1088
+ ackSourceId: run.id,
1089
+ };
1090
+
1091
+ let dispatched: boolean;
1092
+ try {
1093
+ dispatched = await this.handleInboundEvent(event);
1094
+ } catch (err) {
1095
+ this.dispatchedTasks.delete(dedupeKey);
1096
+ throw err;
1097
+ }
1098
+ if (!dispatched) {
1099
+ this.dispatchedTasks.delete(dedupeKey);
1100
+ }
1101
+ return dispatched;
1102
+ }
1103
+
1004
1104
  private async catchUpFromDispatch(coldStart = false) {
1005
1105
  const minAge = coldStart ? Date.now() - this.COLD_START_WINDOW_MS : 0;
1006
1106
  let cursor: string | undefined;
@@ -1053,6 +1153,8 @@ export class ParallAgentGateway {
1053
1153
  }
1054
1154
  } else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
1055
1155
  dispatched = await this.handleTaskComment(item.source_id, item.task_id, item.actor_id);
1156
+ } else if (item.event_type === "schedule.fire" && item.source_id) {
1157
+ dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
1056
1158
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
1057
1159
  if (!this.tryClaimMessage(item.source_id)) continue;
1058
1160
  let msg: Awaited<ReturnType<typeof this.opts.client.getMessage>> | null = null;
@@ -10,7 +10,13 @@
10
10
  * design docs explain WHY the system works the way it does.
11
11
  */
12
12
 
13
- export const PRLL_IDENTITY = `## You on Parall
13
+ export interface AgentIdentity {
14
+ userId: string;
15
+ displayName: string;
16
+ description?: string;
17
+ }
18
+
19
+ const PRLL_IDENTITY_BASE = `## You on Parall
14
20
 
15
21
  Parall is a shared workspace where humans and agents work side by side as equals.
16
22
  You are a participant here, not a service. You hold tasks, own decisions, and are
@@ -20,6 +26,28 @@ The people and agents around you are collaborators, not users to serve. Be hones
20
26
  be direct, and care about the outcome of the work — not just the request in front
21
27
  of you.`;
22
28
 
29
+ function sanitizeProfileField(value: string): string {
30
+ return value.replace(/[\r\n]+/g, " ").replace(/`/g, "'").trim();
31
+ }
32
+
33
+ export function buildIdentity(agent?: AgentIdentity): string {
34
+ if (!agent) return PRLL_IDENTITY_BASE;
35
+ const name = sanitizeProfileField(agent.displayName);
36
+ const lines = [PRLL_IDENTITY_BASE, "", "### Your Parall Identity", ""];
37
+ lines.push(`You are **${name}** (\`prll://${agent.userId}\`).`);
38
+ if (agent.description) {
39
+ lines.push(`Description: ${sanitizeProfileField(agent.description)}`);
40
+ }
41
+ lines.push(
42
+ "",
43
+ `When you see \`${agent.userId}\` or \`prll://${agent.userId}\` in messages, mentions, or events — that's you.`,
44
+ );
45
+ return lines.join("\n");
46
+ }
47
+
48
+ /** @deprecated Use buildIdentity() instead. Kept for backward compat during migration. */
49
+ export const PRLL_IDENTITY = PRLL_IDENTITY_BASE;
50
+
23
51
  export const PRLL_BEHAVIOR = `## How to work here
24
52
 
25
53
  ### Move work forward
@@ -80,7 +108,8 @@ resolves and renders the entity title automatically.
80
108
  prll://tsk_xxx task prll://wik_xxx wiki
81
109
  prll://msg_xxx message prll://tcm_xxx task comment
82
110
  prll://cht_xxx chat prll://ase_xxx agent session
83
- prll://att_xxx attachment
111
+ prll://att_xxx attachment prll://sch_xxx schedule
112
+ prll://srn_xxx schedule run
84
113
 
85
114
  **Wiki** — path is file path, fragment is a typed anchor:
86
115
 
package/src/types.ts CHANGED
@@ -16,7 +16,7 @@ export type DispatchState = {
16
16
 
17
17
  /** Normalized inbound event from Parall. */
18
18
  export type ParallEvent = {
19
- type: "message" | "task" | "task_comment";
19
+ type: "message" | "task" | "task_comment" | "schedule";
20
20
  targetId: string;
21
21
  targetName?: string;
22
22
  targetType?: string;
@@ -32,6 +32,10 @@ export type ParallEvent = {
32
32
  fileSize: number;
33
33
  mimeType: string;
34
34
  }>;
35
- ackSourceType?: "message" | "task_activity" | "comment";
35
+ /** Fire-time snapshot timestamp, used for schedule events. */
36
+ scheduledFireAt?: string;
37
+ /** Fire-time attached_to_uri snapshot, used for schedule events. */
38
+ attachedUri?: string;
39
+ ackSourceType?: "message" | "task_activity" | "comment" | "schedule_run";
36
40
  ackSourceId?: string;
37
41
  };